commit 7ea86a123cb3d96aa4c2c17587d7a56c3a6f970e Author: zc Date: Fri Jun 12 14:59:24 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efa4267 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Maven build output +target/ +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +.flattened-pom.xml + +# Compiled class file +*.class + +# Log file +*.log +logs/ + +# IntelliJ IDEA +.idea/ +*.iws +*.iml +*.ipr + +# Eclipse +.project +.classpath +.settings/ +.metadata/ + +# NetBeans +nbproject/ + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp +*.swp +*~ + +# Environment files +.env +.env.local +.env.*.local + +# Liquibase +*.log.txt + +# Database +*.db +*.sqlite +*.sqlite3 + +sql/ +*.sql diff --git a/.image/qrcode.jpg b/.image/qrcode.jpg new file mode 100644 index 0000000..d95a7aa Binary files /dev/null and b/.image/qrcode.jpg differ diff --git a/.image/screenshot/000登录页面-H5.png b/.image/screenshot/000登录页面-H5.png new file mode 100644 index 0000000..ee50bed Binary files /dev/null and b/.image/screenshot/000登录页面-H5.png differ diff --git a/.image/screenshot/000登录页面.png b/.image/screenshot/000登录页面.png new file mode 100644 index 0000000..b8d8751 Binary files /dev/null and b/.image/screenshot/000登录页面.png differ diff --git a/.image/screenshot/001仪表盘.png b/.image/screenshot/001仪表盘.png new file mode 100644 index 0000000..294ca40 Binary files /dev/null and b/.image/screenshot/001仪表盘.png differ diff --git a/.image/screenshot/002分析页.png b/.image/screenshot/002分析页.png new file mode 100644 index 0000000..1fd7f9b Binary files /dev/null and b/.image/screenshot/002分析页.png differ diff --git a/.image/screenshot/010个人中心.png b/.image/screenshot/010个人中心.png new file mode 100644 index 0000000..8eae084 Binary files /dev/null and b/.image/screenshot/010个人中心.png differ diff --git a/.image/screenshot/011安全设置-修改邮箱.png b/.image/screenshot/011安全设置-修改邮箱.png new file mode 100644 index 0000000..c3bfdde Binary files /dev/null and b/.image/screenshot/011安全设置-修改邮箱.png differ diff --git a/.image/screenshot/012安全设置-修改邮箱-邮箱验证码.png b/.image/screenshot/012安全设置-修改邮箱-邮箱验证码.png new file mode 100644 index 0000000..c28c0f0 Binary files /dev/null and b/.image/screenshot/012安全设置-修改邮箱-邮箱验证码.png differ diff --git a/.image/screenshot/013消息中心.png b/.image/screenshot/013消息中心.png new file mode 100644 index 0000000..d0f1ce4 Binary files /dev/null and b/.image/screenshot/013消息中心.png differ diff --git a/.image/screenshot/020系统管理-用户管理-列表.png b/.image/screenshot/020系统管理-用户管理-列表.png new file mode 100644 index 0000000..efade46 Binary files /dev/null and b/.image/screenshot/020系统管理-用户管理-列表.png differ diff --git a/.image/screenshot/021系统管理-用户管理-新增.png b/.image/screenshot/021系统管理-用户管理-新增.png new file mode 100644 index 0000000..47b84f3 Binary files /dev/null and b/.image/screenshot/021系统管理-用户管理-新增.png differ diff --git a/.image/screenshot/025系统管理-角色管理-列表.png b/.image/screenshot/025系统管理-角色管理-列表.png new file mode 100644 index 0000000..9e248d7 Binary files /dev/null and b/.image/screenshot/025系统管理-角色管理-列表.png differ diff --git a/.image/screenshot/026系统管理-角色管理-新增.png b/.image/screenshot/026系统管理-角色管理-新增.png new file mode 100644 index 0000000..9910fbc Binary files /dev/null and b/.image/screenshot/026系统管理-角色管理-新增.png differ diff --git a/.image/screenshot/027系统管理-角色管理-分配.png b/.image/screenshot/027系统管理-角色管理-分配.png new file mode 100644 index 0000000..78d4e57 Binary files /dev/null and b/.image/screenshot/027系统管理-角色管理-分配.png differ diff --git a/.image/screenshot/030系统管理-菜单管理-列表.png b/.image/screenshot/030系统管理-菜单管理-列表.png new file mode 100644 index 0000000..f1190b7 Binary files /dev/null and b/.image/screenshot/030系统管理-菜单管理-列表.png differ diff --git a/.image/screenshot/031系统管理-菜单管理-新增.png b/.image/screenshot/031系统管理-菜单管理-新增.png new file mode 100644 index 0000000..b8f6a7a Binary files /dev/null and b/.image/screenshot/031系统管理-菜单管理-新增.png differ diff --git a/.image/screenshot/035系统管理-部门管理-列表.png b/.image/screenshot/035系统管理-部门管理-列表.png new file mode 100644 index 0000000..88fb5e8 Binary files /dev/null and b/.image/screenshot/035系统管理-部门管理-列表.png differ diff --git a/.image/screenshot/036系统管理-部门管理-新增.png b/.image/screenshot/036系统管理-部门管理-新增.png new file mode 100644 index 0000000..d6dab35 Binary files /dev/null and b/.image/screenshot/036系统管理-部门管理-新增.png differ diff --git a/.image/screenshot/040系统管理-字典管理-列表.png b/.image/screenshot/040系统管理-字典管理-列表.png new file mode 100644 index 0000000..4d76b60 Binary files /dev/null and b/.image/screenshot/040系统管理-字典管理-列表.png differ diff --git a/.image/screenshot/041系统管理-字典项管理.png b/.image/screenshot/041系统管理-字典项管理.png new file mode 100644 index 0000000..881e13f Binary files /dev/null and b/.image/screenshot/041系统管理-字典项管理.png differ diff --git a/.image/screenshot/045系统管理-公告管理-列表.png b/.image/screenshot/045系统管理-公告管理-列表.png new file mode 100644 index 0000000..73e0dfa Binary files /dev/null and b/.image/screenshot/045系统管理-公告管理-列表.png differ diff --git a/.image/screenshot/046系统管理-公告管理-修改.png b/.image/screenshot/046系统管理-公告管理-修改.png new file mode 100644 index 0000000..660c067 Binary files /dev/null and b/.image/screenshot/046系统管理-公告管理-修改.png differ diff --git a/.image/screenshot/050系统管理-文件管理-列表-1.png b/.image/screenshot/050系统管理-文件管理-列表-1.png new file mode 100644 index 0000000..3d8f51c Binary files /dev/null and b/.image/screenshot/050系统管理-文件管理-列表-1.png differ diff --git a/.image/screenshot/051系统管理-文件管理-列表-2.png b/.image/screenshot/051系统管理-文件管理-列表-2.png new file mode 100644 index 0000000..8d185bf Binary files /dev/null and b/.image/screenshot/051系统管理-文件管理-列表-2.png differ diff --git a/.image/screenshot/052系统管理-文件管理-查看文档.png b/.image/screenshot/052系统管理-文件管理-查看文档.png new file mode 100644 index 0000000..0359647 Binary files /dev/null and b/.image/screenshot/052系统管理-文件管理-查看文档.png differ diff --git a/.image/screenshot/055系统管理-存储管理-列表.png b/.image/screenshot/055系统管理-存储管理-列表.png new file mode 100644 index 0000000..89d5f60 Binary files /dev/null and b/.image/screenshot/055系统管理-存储管理-列表.png differ diff --git a/.image/screenshot/056系统管理-存储管理-新增.png b/.image/screenshot/056系统管理-存储管理-新增.png new file mode 100644 index 0000000..c0647cf Binary files /dev/null and b/.image/screenshot/056系统管理-存储管理-新增.png differ diff --git a/.image/screenshot/060系统管理-系统配置.png b/.image/screenshot/060系统管理-系统配置.png new file mode 100644 index 0000000..b264473 Binary files /dev/null and b/.image/screenshot/060系统管理-系统配置.png differ diff --git a/.image/screenshot/061系统管理-安全配置.png b/.image/screenshot/061系统管理-安全配置.png new file mode 100644 index 0000000..10598f6 Binary files /dev/null and b/.image/screenshot/061系统管理-安全配置.png differ diff --git a/.image/screenshot/100系统监控-在线用户.png b/.image/screenshot/100系统监控-在线用户.png new file mode 100644 index 0000000..b8c05f3 Binary files /dev/null and b/.image/screenshot/100系统监控-在线用户.png differ diff --git a/.image/screenshot/101系统监控-系统日志-登录日志.png b/.image/screenshot/101系统监控-系统日志-登录日志.png new file mode 100644 index 0000000..4428c31 Binary files /dev/null and b/.image/screenshot/101系统监控-系统日志-登录日志.png differ diff --git a/.image/screenshot/102系统监控-系统日志-操作日志.png b/.image/screenshot/102系统监控-系统日志-操作日志.png new file mode 100644 index 0000000..71017e2 Binary files /dev/null and b/.image/screenshot/102系统监控-系统日志-操作日志.png differ diff --git a/.image/screenshot/103系统监控-系统日志-操作日志-详情.png b/.image/screenshot/103系统监控-系统日志-操作日志-详情.png new file mode 100644 index 0000000..80c7964 Binary files /dev/null and b/.image/screenshot/103系统监控-系统日志-操作日志-详情.png differ diff --git a/.image/screenshot/150任务调度-任务管理-列表.png b/.image/screenshot/150任务调度-任务管理-列表.png new file mode 100644 index 0000000..b734ebc Binary files /dev/null and b/.image/screenshot/150任务调度-任务管理-列表.png differ diff --git a/.image/screenshot/151任务调度-任务管理-新增.png b/.image/screenshot/151任务调度-任务管理-新增.png new file mode 100644 index 0000000..bfee241 Binary files /dev/null and b/.image/screenshot/151任务调度-任务管理-新增.png differ diff --git a/.image/screenshot/155任务调度-任务日志-列表.png b/.image/screenshot/155任务调度-任务日志-列表.png new file mode 100644 index 0000000..6fddcfe Binary files /dev/null and b/.image/screenshot/155任务调度-任务日志-列表.png differ diff --git a/.image/screenshot/156任务调度-任务日志-详情.png b/.image/screenshot/156任务调度-任务日志-详情.png new file mode 100644 index 0000000..d692c74 Binary files /dev/null and b/.image/screenshot/156任务调度-任务日志-详情.png differ diff --git a/.image/screenshot/200能力开放-应用管理-列表.png b/.image/screenshot/200能力开放-应用管理-列表.png new file mode 100644 index 0000000..b16adf5 Binary files /dev/null and b/.image/screenshot/200能力开放-应用管理-列表.png differ diff --git a/.image/screenshot/201能力开放-应用管理-新增.png b/.image/screenshot/201能力开放-应用管理-新增.png new file mode 100644 index 0000000..1aa49c8 Binary files /dev/null and b/.image/screenshot/201能力开放-应用管理-新增.png differ diff --git a/.image/screenshot/300系统工具-代码生成-列表.png b/.image/screenshot/300系统工具-代码生成-列表.png new file mode 100644 index 0000000..7845387 Binary files /dev/null and b/.image/screenshot/300系统工具-代码生成-列表.png differ diff --git a/.image/screenshot/301系统工具-代码生成-配置.png b/.image/screenshot/301系统工具-代码生成-配置.png new file mode 100644 index 0000000..1e62a54 Binary files /dev/null and b/.image/screenshot/301系统工具-代码生成-配置.png differ diff --git a/.image/screenshot/302系统工具-代码生成-预览.png b/.image/screenshot/302系统工具-代码生成-预览.png new file mode 100644 index 0000000..e898208 Binary files /dev/null and b/.image/screenshot/302系统工具-代码生成-预览.png differ diff --git a/.style/Java开发手册(黄山版).pdf b/.style/Java开发手册(黄山版).pdf new file mode 100644 index 0000000..6ff47d6 Binary files /dev/null and b/.style/Java开发手册(黄山版).pdf differ diff --git a/.style/license-header b/.style/license-header new file mode 100644 index 0000000..e69de29 diff --git a/.style/p3c-codestyle.xml b/.style/p3c-codestyle.xml new file mode 100644 index 0000000..fb5f47f --- /dev/null +++ b/.style/p3c-codestyle.xml @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e2ef40e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,790 @@ +## [v3.5.0](https://github.com/wms-org/wms-admin/compare/v3.4.1...v3.5.0) (2025-03-05) + +### ✨ 新特性 + +* 【generator】生成预览支持批量 ([a7296a3](https://github.com/wms-org/wms-admin/commit/a7296a36278f68108a0513fe26b3cccd9af5244c)) +* 【generator】代码生成新增 Mapper.xml 模板 ([b519364](https://github.com/wms-org/wms-admin/commit/b51936445d36a76650d94225b3ecfa81b86e066c)) ([98569ae](https://github.com/wms-org/wms-admin/commit/98569ae20530a3c94cd1a071a29fae3c904761e3)) +* 🔥新增终端管理,重构认证体系,多端认证鉴权控制 ([Gitee#40](https://gitee.com/wms/wms-admin/pulls/40)) ([95f2617](https://github.com/wms-org/wms-admin/commit/95f2617a4c086e3e5113799fe213c24c42bfbd17)) ([c90e80e](https://github.com/wms-org/wms-admin/commit/c90e80e9d72eba173df586e7f12a0c9378c82ec9)) ([438615f](https://github.com/wms-org/wms-admin/commit/438615f87c5583890703c78c4e61aae36e3dab80)) ([229bd9b](https://github.com/wms-org/wms-admin/commit/229bd9becfda15cbcb049fd1e0842dbe04d7b135)) ([a305eac](https://github.com/wms-org/wms-admin/commit/a305eac96f766ad3a72051fe19f533f4fd64d304)) ([82cf439](https://github.com/wms-org/wms-admin/commit/82cf4390e8fdc73da1818dcc9a5dd0c7ff87db02)) ([5f68e84](https://github.com/wms-org/wms-admin/commit/5f68e84e7dbb5c2d0f793c72df76d69d326e574e)) +* 【generator】支持源项目内生成代码文件 ([GitHub#125](https://github.com/wms-org/wms-admin/pull/125)) ([653802e](https://github.com/wms-org/wms-admin/commit/653802efbe2debdbfd1fe5097dae7280e80f1e43)) +* 新增接口文档配置,支持显示 SaToken 权限码 ([Gitee#44](https://gitee.com/wms/wms-admin/pulls/44)) ([19c5dbd](https://github.com/wms-org/wms-admin/commit/19c5dbd2659264cfba59e2b1316420c39f82f731)) +* 新增NoHandlerFoundException、HttpRequestMethodNotSupportedException异常处理 ([Gitee#44](https://gitee.com/wms/wms-admin/pulls/44)) ([4efe025](https://github.com/wms-org/wms-admin/commit/4efe025b2e36c56162dc8fcbd05482b7ecc21e5f)) + +### 💎 功能优化 + +- 菜单路由为空时默认返回空列表而非 null ([43cc429](https://github.com/wms-org/wms-admin/commit/43cc429234150e185a697b6dbc340bd966bf6133)) +- 移除初始数据脚本 update_user、update_time 字段赋值(此优化无需跟进升级) ([9d0e1fc](https://github.com/wms-org/wms-admin/commit/9d0e1fc8e642c5be46d20173fd573582ad54d5e6)) +- 【generator】消除前端红色报警、更新表格创建者和更新者字段索引,自定义单选框数据 ([GitHub#108](https://github.com/wms-org/wms-admin/pull/108)) ([4c8ebf2](https://github.com/wms-org/wms-admin/commit/4c8ebf2d0f3e65737f6a55c06eb1ff2d31273505)) +- 更新 nginx.conf 部署配置文件 ([4920d7b](https://github.com/wms-org/wms-admin/commit/4920d7b730c2e975c4a50a5b8b8172f5365509c5)) +- 调整 starter 内的 BaseResp、BaseDetailResp 到 admin 项目 ([144251b](https://github.com/wms-org/wms-admin/commit/144251b21ec0d79927164a705f3c846aace53ca1)) +- 调整 starter 内的 CommonUserService、ContainerPool 到 admin 项目 ([f1d0b49](https://github.com/wms-org/wms-admin/commit/f1d0b491b14d806fbe1d0011cdadea64336fe3b0)) +- 优化登录日志描述 ([a24136d](https://github.com/wms-org/wms-admin/commit/a24136d6fe92bcb055c965600caef399df563361)) +- 丰富部门、角色、用户初始测试数据,方便开发场景 ([b5bbdb2](https://github.com/wms-org/wms-admin/commit/b5bbdb27e6e002d52b5a1894e2bf8e24e6f963e8)) +- 调整 starter 内的 BaseDO、BaseCreateDO、BaseUpdateDO 到 admin 项目 ([498e680](https://github.com/wms-org/wms-admin/commit/498e680672df00be9e8f9546f2010fcc54faab23)) +- 🔥重构角色管理,更新权限扁平化 ([f6535ef](https://github.com/wms-org/wms-admin/commit/f6535ef7a35794147d8094eed71a4789c06f3db8)) ([0a62f81](https://github.com/wms-org/wms-admin/commit/0a62f81ad7c1f7b086379059fe780a609e5d575b)) ([144cfa2](https://github.com/wms-org/wms-admin/commit/144cfa27ce944dc5b78d2d26466cd149b8ca7959)) +- 优化任务调度服务配置,允许用户名密码使用环境变量 ([GitHub#129](https://github.com/wms-org/wms-admin/pull/129)) ([0e65190](https://github.com/wms-org/wms-admin/commit/0e651902f267feb8b4997d066285d979429bd56f)) +- 优化系统配置 SQL 数据脚本 ([d336911](https://github.com/wms-org/wms-admin/commit/d3369119e090b62468fda38e1dfb52ddb5dc7df3)) +- 调整日志 module 字段长度 50 => 100 ([65941c1](https://github.com/wms-org/wms-admin/commit/65941c1ee4020e2f9b4fc07f15c249b9d0d7e851)) +- 🔥重构存储管理,新增设置默认存储、修改状态接口 ([37d6efb](https://github.com/wms-org/wms-admin/commit/37d6efb70e5bcfddc8a4ca6becaf440ab05785fb)) + +### 🐛 问题修复 + +- 【generator】修复 PostgreSQL 菜单 SQL 脚本模板错误 ([GitHub#107](https://github.com/wms-org/wms-admin/pull/107)) ([af403d0](https://github.com/wms-org/wms-admin/commit/af403d055af1c186d7b5976cff894e9dd2afcc01)) +- 【generator】生成菜单脚本添加ID ([GitHub#109](https://github.com/wms-org/wms-admin/pull/109)) ([9ebecdc](https://github.com/wms-org/wms-admin/commit/9ebecdc1935a99f15ece4e2c933175d9919e9825)) +- 【generator】前端页面生成表单类型 ([GitHub#110](https://github.com/wms-org/wms-admin/pull/110)) ([75d2662](https://github.com/wms-org/wms-admin/commit/75d26623652d4813643cea9a7c3f821e44edc885)) +- 完善部分 in 查询前的空集合处理 ([899354a](https://github.com/wms-org/wms-admin/commit/899354a6e7239ed00f81155e8ed5f2760b191480)) +- 修复公告通知范围字段类型错误 ([fdd0617](https://github.com/wms-org/wms-admin/commit/fdd0617a2832221bb017d2e17dc5fb82af862707)) +- 修复通知公告分页 通知范围字段类型回显错误 ([160ab8d](https://github.com/wms-org/wms-admin/commit/160ab8d38bb68801ed6efe0ceeb4ccb4c3f4fba3)) +- 🔥修复 PageResp 手动分页计算错误 ([6bcff72](https://github.com/wms-org/wms-admin/commit/6bcff7244f0a37474c39509dfed44d3cf630d898)) +- 修复导入用户部门名称校验注解使用错误 ([Gitee#41](https://gitee.com/wms/wms-admin/pulls/41)) ([c870014](https://github.com/wms-org/wms-admin/commit/c870014730a47d9a9b567416b2f6736049a25125)) +- 修复 PostgreSQL Liquibase 数据脚本缺失 ([8c53700](https://github.com/wms-org/wms-admin/commit/8c53700cfd8ea20ccece1867161d2315d5346d9b)) +- 修复新增用户时日志记录获取 description 为空的问题 ([91924ac](https://github.com/wms-org/wms-admin/commit/91924acaa15658db88c387f81ff723ca043cd1f1)) +- 调整 PostgreSQL 连接配置以消除部分类型使用报错 ([7e3257b](https://github.com/wms-org/wms-admin/commit/7e3257bd6d3965622ba53a906fd8b27e5209e67f)) +- 修复部分过期配置信息 ([3fb9922](https://github.com/wms-org/wms-admin/commit/3fb9922b524a5c1a40de5bd011c99c9863032f7a)) +- 修复邮箱登录,手机号登录对应日志没有记录操作人问题 ([Gitee#42](https://gitee.com/wms/wms-admin/pulls/42)) ([aab3931](https://github.com/wms-org/wms-admin/commit/aab3931f3078c1b3468c5f06e1f133862f947d7e)) +- mysql 8.x failing to connect to the database correctly issue ([GitHub#128](https://github.com/wms-org/wms-admin/pull/128)) ([4caada8](https://github.com/wms-org/wms-admin/commit/4caada8c64c1f6646f312a0038338396ae860305)) + +### 📦 依赖升级 + +- 🔥ContiNew Starter 2.7.5 => 2.9.0 (更多特性及依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v3.4.1](https://github.com/wms-org/wms-admin/compare/v3.4.0...v3.4.1) (2024-12-08) + +### ✨ 新特性 + +* 新增验证码配置开关 ([e314346](https://github.com/wms-org/wms-admin/commit/e31434617e751f08d12ace7773bb3ba7bf132370)) ([61fe39d](https://github.com/wms-org/wms-admin/commit/61fe39d439b73c90cfeb989f8f4727ade6b5b3b2)) (Gitee#37@aiming317) + +### 💎 功能优化 + +- 【open】优化 API 参数签名处理 ([22b3564](https://github.com/wms-org/wms-admin/commit/22b3564a2217dee739fc2172453b23600d82d6de)) +- 移除关于项目菜单初始数据(该菜单从动态路由调整为前端静态,且不再需要鉴权) ([88313c8](https://github.com/wms-org/wms-admin/commit/88313c8b2017e7ec620e5372f34bf5e431ce3e7f)) +- 优化代码生成菜单图标 ([9296985](https://github.com/wms-org/wms-admin/commit/9296985be0ab63ba54c63c71c011681f91aef7fb)) +- BaseServiceImpl 所在包调整 ([d7ae7b4](https://github.com/wms-org/wms-admin/commit/d7ae7b4e42c424a3db51c72a0ed79572c9fd7601)) +- BaseController 改为在 Admin common 模块编写(重构权限校验 checkPermission 处理) ([d7ae7b4](https://github.com/wms-org/wms-admin/commit/d7ae7b4e42c424a3db51c72a0ed79572c9fd7601)) +- CRUD ValidateGroup => CrudValidationGroup ([d7ae7b4](https://github.com/wms-org/wms-admin/commit/d7ae7b4e42c424a3db51c72a0ed79572c9fd7601)) +- ValidateGroup => ValidationGroup ([d7ae7b4](https://github.com/wms-org/wms-admin/commit/d7ae7b4e42c424a3db51c72a0ed79572c9fd7601)) + +### 🐛 问题修复 + +- 【generator】修复 columnSize 类型错误,兼容无注释字段配置 ([6b64ae3](https://github.com/wms-org/wms-admin/commit/6b64ae3e07a76d844eec4bd05302126cbcaca31b)) +- 补充能力开放模块接口文档配置 ([270fbf1](https://github.com/wms-org/wms-admin/commit/270fbf15af338a6ac3e6a686409eea8e9a32b6bf)) +- 修复文件管理删除文件异常或不成功的情况 ([361a412](https://github.com/wms-org/wms-admin/commit/361a41258e9fdece5ba681298f2839b013d6cfab)) (Gitee#35@kiki1373639299) +- 修复本地文件管理删除文件异常或不成功的情况 ([c7b58a0](https://github.com/wms-org/wms-admin/commit/c7b58a0fd167c566f6680c87cc455b71c42b8eda)) (Gitee#36@kiki1373639299) +- 修复 Query 查询数组范围报错 ([d7ae7b4](https://github.com/wms-org/wms-admin/commit/d7ae7b4e42c424a3db51c72a0ed79572c9fd7601)) + +### 📦 依赖升级 + +- ContiNew Starter 2.7.4 => 2.7.5 (更多特性及依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v3.4.0](https://github.com/wms-org/wms-admin/compare/v3.3.0...v3.4.0) (2024-11-18) + +### ✨ 新特性 + +* 新增仪表盘分析接口,查询访问时段分析、查询模块分析、查询终端分析、查询浏览器分析 ([dea8dbe](https://github.com/wms-org/wms-admin/commit/dea8dbe131867a564f7e151a6484db5be6effaa3)) +* 新增查询仪表盘数据总览相关接口, 重构仪表盘相关代码 ([e01df09](https://github.com/wms-org/wms-admin/commit/e01df09127e6efc33971d64e2fe49a2a42282425)) +* 公告支持设置通知范围 ([29202ae](https://github.com/wms-org/wms-admin/commit/29202aea307a7257c9d1e9649dee00140164c59c)) (GitCode#1) +* 角色管理增加分配角色功能 ([73f880e](https://github.com/wms-org/wms-admin/commit/73f880ec57cfdccfc297aac228410f5bb7fed448)) ([ad3f832](https://github.com/wms-org/wms-admin/commit/ad3f8329dd07858b0982020db3605200112f09b5)) (GitHub#93) +* 新增能力开放模块应用管理功能 ([f774183](https://github.com/wms-org/wms-admin/commit/f7741832bdd315039cc5f1aa062d8ffac32ddf0f)) ([d1b3824](https://github.com/wms-org/wms-admin/commit/d1b38242b9f291c897a8eb82bd330bd775e656cf)) ([4454daa](https://github.com/wms-org/wms-admin/commit/4454daa9e07af7c7202c386e533ea055bb79f0df)) (Gitee#31) +* 新增查询用户字典接口 ([d4b02ba](https://github.com/wms-org/wms-admin/commit/d4b02ba9180f82084e5ca844eaa9b4a0966a0164)) +* 代码生成新增菜单SQL模板 ([fb947c9](https://github.com/wms-org/wms-admin/commit/fb947c98fdd075a19c55e1e9e5c137a8482db618)) (GitHub#95) + +### 💎 功能优化 + +- 优化部分 Mapper 方法使用 (替换为 MP 新增方法) ([ad69d44](https://github.com/wms-org/wms-admin/commit/ad69d44ebda72dbdb639ba4ff48cde9aa7f6e400)) +- 优化查询代码生成表性能 ([781d291](https://github.com/wms-org/wms-admin/commit/781d29142fbcc1f981d3760565f2f96b49570438)) +- 移除上传文件时的多余校验 ([8466105](https://github.com/wms-org/wms-admin/commit/8466105a9b8b2fd807ac9d3029e4da7bc609d551)) +- 重构获取登录用户信息方式(线程级存储) ([79ea39d](https://github.com/wms-org/wms-admin/commit/79ea39dd078639b4e137d576f3d7820bb6c24d0a)) +- 完善及优化代码生成模板 ([ffdc971](https://github.com/wms-org/wms-admin/commit/ffdc9712d4cd1fd3093cec0780f630d672339cdf)) ([2b47ed7](https://github.com/wms-org/wms-admin/commit/2b47ed711074ad64d98fa0d9d68ccd2777b70bf2)) ([90e3bc0](https://github.com/wms-org/wms-admin/commit/90e3bc0595fdb692644ed7fd9b1a3735962cb68b)) ([985bc25](https://github.com/wms-org/wms-admin/commit/985bc25716daae6fb018a856af889919436f26e6)) +- 字典项管理日志模块调整为字典管理 ([60cb2e3](https://github.com/wms-org/wms-admin/commit/60cb2e3b5cae56d9a3117aeea6c57ec02cb9abe4)) +- 解决查询日志数据时索引失效的问题 ([4525cb3](https://github.com/wms-org/wms-admin/commit/4525cb3531c06354e3dc57147c93dd5f7f8a4e8a)) +- 重构拆分 liquibase 脚本结构 ([aadaa5b](https://github.com/wms-org/wms-admin/commit/aadaa5b4a70caaa775a38fd542890aa8d7f951c6)) +- 调整系统配置菜单图标 ([872bc1c](https://github.com/wms-org/wms-admin/commit/872bc1ca8143632ea0ca00eb320e57f61729937c)) +- 优化系统管理、代码生成相关代码及初始数据脚本 ([9ecdeb5](https://github.com/wms-org/wms-admin/commit/9ecdeb52f601b93116f6e89d8db32d8db95cb0c5)) ([5717d03](https://github.com/wms-org/wms-admin/commit/5717d03d01f8f052688bef873def83a3b8defc21)) ([7870de2](https://github.com/wms-org/wms-admin/commit/7870de28926cde86a8732ad5d6950616e1078d57)) +- 优化项目模块命名(简化、分类、统一) ([c276e53](https://github.com/wms-org/wms-admin/commit/c276e53a8e02e64f9e5d8270171e20424804382d)) +- 优化任务调度配置及 docker 部署脚本 ([b927470](https://github.com/wms-org/wms-admin/commit/b927470e33bb75594f1546a4f06d48bc2281f5ab)) ([c5cd4e2](https://github.com/wms-org/wms-admin/commit/c5cd4e2c284fa195411f704e41ca50a70271c6b7)) +- 重构仪表盘查询地域分析接口 ([e0e157f](https://github.com/wms-org/wms-admin/commit/e0e157f0e5b23ff6e27ec7ae0ee028af6f2facd1)) +- 完善 PostgreSQL 代码生成类型映射配置 ([4c36f23](https://github.com/wms-org/wms-admin/commit/4c36f2339830272aa047e46d02f34485b9051ec3)) +- 优化通知公告部分代码 ([e1941ec](https://github.com/wms-org/wms-admin/commit/e1941eca455a066dae631b1533d63f8d9a193161)) +- 优化初始数据脚本 ([6abb444](https://github.com/wms-org/wms-admin/commit/6abb444f9dce7ed1ea4aa90502c21d2ab6c8e247)) +- 忽略获取在线用户信息异常 ([4856366](https://github.com/wms-org/wms-admin/commit/48563663e1fed93154f63f6c9a6c07ea79d741da)) +- 优化部分注释 ([3116836](https://github.com/wms-org/wms-admin/commit/3116836b0139232769797da271a400fa4d9d52fa)) + +### 🐛 问题修复 + +- 参数配置支持设值为空 ([d7e8fc9](https://github.com/wms-org/wms-admin/commit/d7e8fc9bc31409b3652b5ad03a79248726547088)) +- 修复修改存储时同时设置默认存储及启用判断顺序错误 ([d9602e8](https://github.com/wms-org/wms-admin/commit/d9602e8639bcd125b15faea5ca7f618429bcc50e)) +- 修复任务日志缺失异常堆栈的问题 ([5cbeddb](https://github.com/wms-org/wms-admin/commit/5cbeddb97bd38274641ac5d63226a937975d69ba)) (Gitee#29) +- 修复更新在线用户权限信息报错的问题 ([8278032](https://github.com/wms-org/wms-admin/commit/82780324b7c2faeba22f2dbde440d1cf0e42c3c9)) +- 修复查询日志排序错误 ([8b403f4](https://github.com/wms-org/wms-admin/commit/8b403f4357caeac0c0a4cc9ac67c73043a1f4465)) +- 修复部分错误规范代码 ([a83b45f](https://github.com/wms-org/wms-admin/commit/a83b45f776234274a844337a2f2b541705ba5aff)) +- 调整部分实体包 ([3f4331e](https://github.com/wms-org/wms-admin/commit/3f4331e92b86e73303c4d675f0f1d4bc91a2a71b)) +- 修复获取邮箱验证码未进行行为验证码校验错误 ([731bfa0](https://github.com/wms-org/wms-admin/commit/731bfa065ab3a10ab933aaffd2e9ceebf0a4d16d)) +- 完善用户角色变更校验及在线用户权限处理 ([c28d3cf](https://github.com/wms-org/wms-admin/commit/c28d3cf1c45212e670b90fc0077b5e176a894bd2)) +- 修复查询系统配置参数漏洞 :boom: ([8c3fe35](https://github.com/wms-org/wms-admin/commit/8c3fe353be5d68f1ed252eef12f5fcdc0a1e3c83)) + +### 📦 依赖升级 + +- ContiNew Starter 2.6.0 => 2.7.4 (更多特性及依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v3.3.0](https://github.com/wms-org/wms-admin/compare/v3.2.0...v3.3.0) (2024-09-09) + +### ✨ 新特性 + +* 重构全局响应处理及异常拦截,自定义异常拦截从 Starter 调整到 Admin 项目 ([d7621c6](https://github.com/wms-org/wms-admin/commit/d7621c6b26bfd253d9295444ea144f5dabf67f44)) +* 重构 Controller 接口方法返回值写法,接口文档也已适配处理 ([d7621c6](https://github.com/wms-org/wms-admin/commit/d7621c6b26bfd253d9295444ea144f5dabf67f44)) ([0f1479f](https://github.com/wms-org/wms-admin/commit/0f1479f40deef83a5f5d64cbc24a7691b274b112)) +* 代码生成字段配置时支持指定排序 ([d56b9aa](https://github.com/wms-org/wms-admin/commit/d56b9aa35ee2502804f487487d7bac02f4edc9b0)) +* 代码生成字段配置时支持选择关联字典 ([fdd21a0](https://github.com/wms-org/wms-admin/commit/fdd21a01c106e12321d0d1886ff643c72a09943b)) ([ecc98b1](https://github.com/wms-org/wms-admin/commit/ecc98b1999d90c1a7a29af94dc8705283f34dada)) +* 修改角色功能权限、数据权限支持衔接新增角色时的父子联动选项 ([387fb19](https://github.com/wms-org/wms-admin/commit/387fb194640d4a288f053c3eba1bf5b314d64da7)) + +### 💎 功能优化 + +- 移除 WebMvcConfiguration 配置(已迁移到 Starter 项目)([d7621c6](https://github.com/wms-org/wms-admin/commit/d7621c6b26bfd253d9295444ea144f5dabf67f44)) +- 重构日志持久层接口本地实现类 ([2c1eb56](https://github.com/wms-org/wms-admin/commit/2c1eb5660f69a9ab702d503944a11e47edac1142)) +- 优化打包配置,模板等配置文件提取到 jar 包外部 ([75cef77](https://github.com/wms-org/wms-admin/commit/75cef773187e5b5060a10a12e7c9912002376d7a)) +- 优化健康监测接口响应信息 ([bb5a92e](https://github.com/wms-org/wms-admin/commit/bb5a92e5ca238ed677d9ac3589fdf8009d2ac232)) +- 优化代码生成列配置代码,取消后端部分默认值 ([f5ee2b5](https://github.com/wms-org/wms-admin/commit/f5ee2b5beb9572d3fcd5b7c2f6db0627dedb31aa)) ([ca9f34d](https://github.com/wms-org/wms-admin/commit/ca9f34d3d5a3f96c6df537036a5fd876cae2e89a)) +- 重构权限变更逻辑,修改角色、变更用户角色不再下线用户 ([ad9a600](https://github.com/wms-org/wms-admin/commit/ad9a6000fcb5d64b04cf230caa3cbacc8c3ac8d7)) + +### 🐛 问题修复 + +- 修复打包部署后,下载用户导入模板异常问题 (Gitee#25) ([c7ffc67](https://github.com/wms-org/wms-admin/commit/c7ffc67cdc9139a4398c7dc819ca453880bd100a)) +- 修复日志记录仅支持获取 JSON 结构响应体的问题 ([d7621c6](https://github.com/wms-org/wms-admin/commit/d7621c6b26bfd253d9295444ea144f5dabf67f44)) +- 修复并增强 SQL 注入防御 ([0f1479f](https://github.com/wms-org/wms-admin/commit/0f1479f40deef83a5f5d64cbc24a7691b274b112)) +- 修复目录、菜单的组件名称重复的错误问题 ([9e91f56](https://github.com/wms-org/wms-admin/commit/9e91f563e2a263ce302dc3bf17c89e37c2b56285)) +- 修复 DataPermission 注解表别名配置无效的问题 ([6c4e252](https://github.com/wms-org/wms-admin/commit/6c4e2522df3f44ba0f5a21228e805b8ac98f8e6b)) +- 临时移除 MyBatis Plus saveBatch 不兼容的 rewriteBatchedStatements 配置 ([25240fa](https://github.com/wms-org/wms-admin/commit/25240fa81957a1677deda294ce8f2b0af5413315)) +- 修复更新会导致原加密失效的问题 ([8903195](https://github.com/wms-org/wms-admin/commit/89031954c0b7daee1c08e1a10fd50139301cd6ab)) ([c87317d](https://github.com/wms-org/wms-admin/commit/c87317d19946989e86dfbc5f24b155b2ea5abdc9)) +- 修复角色查询参数与前端不一致的问题 ([098571f](https://github.com/wms-org/wms-admin/commit/098571ffb2febc6163d2b9e5b18c4796ea80cbfa)) +- 修复特殊校验异常不打印堆栈 ([c87317d](https://github.com/wms-org/wms-admin/commit/c87317d19946989e86dfbc5f24b155b2ea5abdc9)) +- 修复日志全局 includes 配置会被局部修改的问题 ([c87317d](https://github.com/wms-org/wms-admin/commit/c87317d19946989e86dfbc5f24b155b2ea5abdc9)) +- 修复初始数据错误 ([403c72a](https://github.com/wms-org/wms-admin/commit/403c72aa52a0f0852208f15fa1c7117ee26414f0)) + +### 📦 依赖升级 + +- ContiNew Starter 2.4.0 => 2.6.0 (更多特性及依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v3.2.0](https://github.com/wms-org/wms-admin/compare/v3.1.0...v3.2.0) (2024-08-05) + +### ✨ 新特性 + +* 新增用户批量导入功能 (GitHub#78) ([c2ad055](https://github.com/wms-org/wms-admin/commit/c2ad055cf82187e132e4cde9e15251e554deadff)) +* 新增任务调度模块 SnailJob(灵活,可靠和快速的分布式任务重试和分布式任务调度平台) (Gitee#22) ([ce1acea](https://github.com/wms-org/wms-admin/commit/ce1acea1535083345c33bcc427054831faf5d2e3)) ([ed5594b](https://github.com/wms-org/wms-admin/commit/ed5594b31e1d31b2a9210b65838d545067ca812f)) ([797221b](https://github.com/wms-org/wms-admin/commit/797221b4dc66c582b95b872b3cb60429247b14e9)) ([7b381b3](https://github.com/wms-org/wms-admin/commit/7b381b36de4ed4ef657249028188abc2f274b036)) ([ffe75e1](https://github.com/wms-org/wms-admin/commit/ffe75e111eb333236e923a1ed14dae5257b09357)) ([cef5cb4](https://github.com/wms-org/wms-admin/commit/cef5cb4fa5e6ee2325fd3738e1c8601a75277dd8)) ([513d8d9](https://github.com/wms-org/wms-admin/commit/513d8d9324b952438ec5513e9e3c7dfb092d5b83)) +* 修改 sys_option sql 脚本以适配 base64 图片 (Gitee#25) ([6848559](https://github.com/wms-org/wms-admin/commit/68485596c47a884b453e4632b59b525527293e17)) + +### 💎 功能优化 + +- 优化更新手机号、邮箱语句 ([9995bf0](https://github.com/wms-org/wms-admin/commit/9995bf0200e6256ccc4a26d8847e37fb85b4a226)) +- 重构适配 ContiNew Starter 最新线程池配置 ([5604fe9](https://github.com/wms-org/wms-admin/commit/5604fe95784b2627f1c8144144546de05434577e)) +- 获取短信、邮箱验证码接口适配 ContiNew Starter 限流器 ([44811fc](https://github.com/wms-org/wms-admin/commit/44811fc93283f508953f5fc9193c0b03305da5b2)) +- 移动 SaToken 配置到 webapi 模块 ([d733b7f](https://github.com/wms-org/wms-admin/commit/d733b7f1661a5f0df2b6f12b33ba69da8810e0aa)) +- 新增 wms-admin-plugins 插件模块,代码生成迁移到插件模块,为后续插件化改造铺垫 ([52f3be8](https://github.com/wms-org/wms-admin/commit/52f3be8ee3a07334c76cdfc06d15698b7ccd65ea)) +- 使用分组校验优化存储管理 ([3a23db1](https://github.com/wms-org/wms-admin/commit/3a23db1a4bdfefc26de67ed9e0317097699d5db6)) +- 移动日志配置和依赖至 webapi 模块 ([48aae87](https://github.com/wms-org/wms-admin/commit/48aae877646e93f20a43fe4002d7f19fa98897c3)) +- 调整部分 Query 查询参数类型为对应枚举(目前已支持非 JSON 格式枚举参数转换) ([f80316e](https://github.com/wms-org/wms-admin/commit/f80316e34d9757225a1a7b6002061e8626018e47)) +- 调整部分枚举类的包位置 ([6b69dd4](https://github.com/wms-org/wms-admin/commit/6b69dd43e1544bd955901d6110fa7d7f65aaa80c)) +- 更新通知公告新增、查看菜单数据 ([4554526](https://github.com/wms-org/wms-admin/commit/45545260a36b57b594eb8329e95b7552cf6893f5)) + +### 🐛 问题修复 + +- 修复代码生成前端模板部分错误 (Gitee#20) ([b512ea9](https://github.com/wms-org/wms-admin/commit/b512ea99f39aaac04bd4db4a9d73e29ddb340d9e)) +- 修复文件管理删除图片时未删除缩略图的问题 ([bc523eb](https://github.com/wms-org/wms-admin/commit/bc523eba30a500a4af62adbd590446c48a5cb0be)) +- 修复存储管理私有密钥校验错误 ([eb65cff](https://github.com/wms-org/wms-admin/commit/eb65cff4c776a8d3e259f8c96d2918acfe038b6a)) +- 删除用户未删除用户历史密码 ([f53d6b6](https://github.com/wms-org/wms-admin/commit/f53d6b6504d5d504581e2697589dcb9b8fbe82ef)) +- 修复菜单缓存更新错误 ([10ff4ce](https://github.com/wms-org/wms-admin/commit/10ff4ce838b950df42994de4dbd2af20ff254949)) +- 修复偶发性报错 zip file closed ([b587cb8](https://github.com/wms-org/wms-admin/commit/b587cb82aa5d48548b5ce75dd4863af037ae8274)) +- 修复代码生成器前端新增数据模板错误 ([81de8d0](https://github.com/wms-org/wms-admin/commit/81de8d060ba081d14873d14d6e4083302a718bef)) + +### 📦 依赖升级 + +- ContiNew Starter 2.1.0 => 2.4.0 (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v3.1.0](https://github.com/wms-org/wms-admin/compare/v3.0.1...v3.1.0) (2024-06-16) + +### ✨ 新特性 + +* 系统配置新增安全设置功能,支持多种密码策略配置,例如:有效期、密码重复使用次数、密码错误锁定等 (GitHub#61) ([1de2a8f](https://github.com/wms-org/wms-admin/commit/1de2a8f2dcf01ef8eeb7e2d662c09af00b38ffb1)) ([90ecaab](https://github.com/wms-org/wms-admin/commit/90ecaab63241b8d351ebaffa4faece8c609882b1)) ([3994142](https://github.com/wms-org/wms-admin/commit/3994142ace6cd6ce9d094b8a4ceed080a2b2ec33)) ([1427c13](https://github.com/wms-org/wms-admin/commit/1427c13b7a0b4e0f569ccc20a3172ad748c98e14)) ([5f5fee6](https://github.com/wms-org/wms-admin/commit/5f5fee63f8e584ffd9542bf132ee724762187d83)) ([c1e9d31](https://github.com/wms-org/wms-admin/commit/c1e9d318e0643c0854a848425955948d05b158d3)) ([48d0f47](https://github.com/wms-org/wms-admin/commit/48d0f476149ff7bc30fe05fe3dec84e5a947cf19)) +* 图片文件支持缩略图 (GitHub#63) ([d320c95](https://github.com/wms-org/wms-admin/commit/d320c9596a321dce1028ac36d23bcd04535e87a6)) ([d44fb3a](https://github.com/wms-org/wms-admin/commit/d44fb3a681370b5763f028cc761fbb5fc94a104c)) +* 在线用户增加最后活跃时间显示 ([926497a](https://github.com/wms-org/wms-admin/commit/926497a18acbb6ef170de9f68bd97e025e212f3e)) +* 新增 WebSocket 消息通知,站内信重新上线 (GitHub#67) ([9970c46](https://github.com/wms-org/wms-admin/commit/9970c461cce5c710f1f8479d4bdd258e65558256)) ([94168e2](https://github.com/wms-org/wms-admin/commit/94168e246f75364605b6fbdb82c9438e2b959c61)) ([5abdb8d](https://github.com/wms-org/wms-admin/commit/5abdb8d86161e7bd03ace14b3c899f6ad13020e2)) +* 文件上传按日期拆分目录 (GitHub#68) ([08aa085](https://github.com/wms-org/wms-admin/commit/08aa08550589876a821a37d56a5fae8867292978)) +* 代码生成增加了 TREE_SELECT/CHECK_GROUP/INPUT_NUMBER/INPUT_PASSWORD控件 (Gitee#17) ([8632b22](https://github.com/wms-org/wms-admin/commit/8632b22bd5b6d43e31b6aeac930836464849441b)) ([cf18c10](https://github.com/wms-org/wms-admin/commit/cf18c1046b77c9f38d28b0e6608d345df3bbd5a9)) +* 系统参数新增根据类别查询方法 ([694cbb2](https://github.com/wms-org/wms-admin/commit/694cbb2850c05f7eb35fb982530a6c204f1f64b0)) +* 支持动态邮件 ([1dbb339](https://github.com/wms-org/wms-admin/commit/1dbb33935a05e9fde749191e34682e632c8e1e63)) + +### 💎 功能优化 + +- 优化部分命名 ([a3cf39f](https://github.com/wms-org/wms-admin/commit/a3cf39f9f8611f7f34dd9f55166d0fe75b72145b)) +- 优化代码生成预览 (Gitee#14) ([ad7412f](https://github.com/wms-org/wms-admin/commit/ad7412f9cbee5d851d060d8e9d717b2553d3d4cb)) +- 优化个人中心部分参数命名 ([61dd3a4](https://github.com/wms-org/wms-admin/commit/61dd3a4c3aef2d4a873a1d4de13bc1962179eeb1)) +- 根据前端最新 ESLint 配置优化代码生成模板 ([044b4b6](https://github.com/wms-org/wms-admin/commit/044b4b669aa2b77b3f5ac27095202e57045fd10d)) +- 优化代码生成模板 ([3ddcdf0](https://github.com/wms-org/wms-admin/commit/3ddcdf0f67d12250da4b7f6f6ff3d63a880e6d4c)) ([6396e9a](https://github.com/wms-org/wms-admin/commit/6396e9a7364bf899023c2399083b43c67efb30cf)) ([2fb4001](https://github.com/wms-org/wms-admin/commit/2fb40015c1ca2bc58b66891e2cfbfac70b029016)) +- 使用 Crane4j 优化在线用户数据填充 ([cb81135](https://github.com/wms-org/wms-admin/commit/cb811350f36364fcc85355c081b8b60c7d5bfb2a)) +- 用户角色名称调整为角色名称列表返回,并全局优化 Crane4j 组件的使用方式 ([857a1c9](https://github.com/wms-org/wms-admin/commit/857a1c90838c8305f916af9dccfd4ca847ca8c66)) ([0b76d5c](https://github.com/wms-org/wms-admin/commit/0b76d5ca33f5900862d48321e8384ee8ded6ae4e)) +- 优化部分方法排序 ([651cc8a](https://github.com/wms-org/wms-admin/commit/651cc8ae71bd3f544bd41adac0ef7044011300a7)) +- 字典管理分页查询接口 => 查询列表接口 ([b13d0e9](https://github.com/wms-org/wms-admin/commit/b13d0e9ee530fc1a8b382e08ead065989f2b0e7f)) +- 移除部门响应信息中的 getDisabled 方法 ([659144a](https://github.com/wms-org/wms-admin/commit/659144afdaf5d1b4667437053a770cf39681cdd7)) +- 文件管理存储路径改为相对路径 (GitHub#69) ([8854f20](https://github.com/wms-org/wms-admin/commit/8854f20ce90f6a1c379ac81a27bb40784a838095)) +- 查询文件列表增加存储名称信息返回 ([69bc1e5](https://github.com/wms-org/wms-admin/commit/69bc1e52e122c14a695d2e37589d36cbb64de8a4)) +- 系统参数表结构新增ID、类别字段 ([45396f2](https://github.com/wms-org/wms-admin/commit/45396f2dc23b4de47912c1d77b05711c839672ce)) +- 优化公告状态判断 ([a07aedb](https://github.com/wms-org/wms-admin/commit/a07aedbf35e32f3d09d2e9ed8857b9a9d2e2e13a)) +- 重构系统参数相关接口 ([6d0060b](https://github.com/wms-org/wms-admin/commit/6d0060b21c374afd9880e57490345a140187a7cd)) +- 优化用户及部门查询 ([448f9a0](https://github.com/wms-org/wms-admin/commit/448f9a0a819d6816a5ae3ada2e07690a5f70f7df)) +- 用户头像改为Base64存储 ([969216d](https://github.com/wms-org/wms-admin/commit/969216d7c67332eae826c11233c8745d9d3ad81c)) ([513ea83](https://github.com/wms-org/wms-admin/commit/513ea83152708194348afeb93d31aed1a9914e57)) ([7a6cafc](https://github.com/wms-org/wms-admin/commit/7a6cafc6e4ff6914fa62a870538b51759af866a8)) +- 优化配置文件 ([5b3d4f5](https://github.com/wms-org/wms-admin/commit/5b3d4f57788e30b62a1e1af9c2620a4cc8659bfe)) +- 优化登录 Helper ([afbd619](https://github.com/wms-org/wms-admin/commit/afbd619d098ff3d70fc786db9594cebf03c07e10)) +- 重构查询参数及字典接口 ([1d60213](https://github.com/wms-org/wms-admin/commit/1d602134377fc13b062981476b8c17947b36006d)) +- 重构查询角色字典接口 ([1e73d06](https://github.com/wms-org/wms-admin/commit/1e73d06a972d380bbed253eaffd806d8e0698525)) +- 使用 CompletableFuture 实现异步加载用户权限、角色代码和角色信息,以提高登录时的性能和响应速度 ([d5f3c74](https://github.com/charles7c/wms-admin/commit/d5f3c7417ad7b1178b5a53da3fd6f3cb7cd3b19a)) + + +### 🐛 问题修复 + +- 补充查询文件资源统计权限校验注解 ([60cbf04](https://github.com/wms-org/wms-admin/commit/60cbf0402a350d05a09abec57ba94c7758b46499)) +- Postgresql startup script fixes (GitHub#60) ([8caad16](https://github.com/wms-org/wms-admin/commit/8caad16ef226e473a81f79b82485c3d03ded7a42)) +- 修复初始菜单数据错误 ([f062797](https://github.com/wms-org/wms-admin/commit/f062797629bd7fc220ceae1e44859146ef4a14ff)) +- 字典编码、存储编码及类型、菜单类型不允许修改 ([79d0101](https://github.com/wms-org/wms-admin/commit/79d0101e5eb1baa86979b6a6d3c584a2a483320f)) +- 修复行为验证码接口请求次数限制 ([573e634](https://github.com/wms-org/wms-admin/commit/573e634b433c473551244418e695a34bbd3fa675)) +- 修复导出用户报错 ([655a695](https://github.com/wms-org/wms-admin/commit/655a695753d12db624fbf2afaf10d38f67241e31)) +- 移除部门名称错误正则 ([0285874](https://github.com/wms-org/wms-admin/commit/0285874540c0cadc8aae74077f6368b6e7977c35)) +- 修复插入第三方登录用户时报错 ([0cfc7a5](https://github.com/wms-org/wms-admin/commit/0cfc7a5c80c3558028c7225fc459ecb07149ab0d)) +- 修复更新手机号、邮箱未加密的问题 ([485d708](https://github.com/wms-org/wms-admin/commit/485d708cd45df922ec8e601b7bd7344e9ebd9299)) ([e6d7205](https://github.com/wms-org/wms-admin/commit/e6d720571d3273f27351e81a19fdd97b9b4336f6)) + +### 📦 依赖升级 + +- ContiNew Starter 2.0.0 => 2.1.0 (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v3.0.1](https://github.com/wms-org/wms-admin/compare/v3.0.0...v3.0.1) (2024-05-03) + +### ✨ 新特性 + +* 新增验证码超时显示效果,超时后显示已过期请刷新 (GitHub#56) ([4c6a7fb](https://github.com/wms-org/wms-admin/commit/4c6a7fb91ad195b86d776f8aef6aef81d07b2eb1)) +* 文件管理增加资源统计,统计总存储量、各类型文件存储占用 (GitHub#58) ([15c966f](https://github.com/wms-org/wms-admin/commit/15c966f7bb255db3edea249f8d3354324cbdbf5b)) + +### 💎 功能优化 + +- 获取图片验证码 URL /img => /image ([9a1a472](https://github.com/wms-org/wms-admin/commit/9a1a472ec996362cb918e79b9ce37bfa2639a10b)) +- 移除对部分 API 重复的权限校验 ([53eaef9](https://github.com/wms-org/wms-admin/commit/53eaef9fbdfd6d0866a3d5e424d783e2e7bc0e17)) +- 优化代码生成模板 ([dc92731](https://github.com/wms-org/wms-admin/commit/dc9273132dc8e266f2d44c834b9c2733256afdfe)) ([def831f](https://github.com/wms-org/wms-admin/commit/def831f2dca0703f5ef8b84b0e695a32b171461d)) + + +### 🐛 问题修复 + +- 修复查询用户邮箱、手机号时未自动加密导致的错误 ([faa56d1](https://github.com/wms-org/wms-admin/commit/faa56d16b92cbdb8f7e16c8b43c2916ae692d881)) +- 修复根据部门查询用户列表数据错误 ([42ac82e](https://github.com/wms-org/wms-admin/commit/42ac82e7ceef9336741c2514470c0db36ab7075e)) +- 修复文件类型处理错误 ([9b60e24](https://github.com/wms-org/wms-admin/commit/9b60e24364bfb4cc7cd9996a43579a062197cdf3)) + +## [v3.0.0](https://github.com/wms-org/wms-admin/compare/v2.5.0...v3.0.0) (2024-04-27) + +### ✨ 新特性 + +* 系统日志新增导出 API ([bd0f40c](https://github.com/wms-org/wms-admin/commit/bd0f40c6ad397174baf80b04923ef1e94ff28e3c)) +* 适配 3.0 前端菜单,并梳理菜单数据 +* 适配 3.0 前端代码生成模板,代码预览及生成 ([3dbe72f](https://github.com/wms-org/wms-admin/commit/3dbe72fd570c44b32599d869abd30331137a6c7d)) + +### 💎 功能优化 + +- 重构日志管理相关接口 ([7793f82](https://github.com/wms-org/wms-admin/commit/7793f82009bcdb5fcdfe5e91daab211ab1705bf7)) +- 优化部门管理相关 API,合并 DeptResp 及 DeptDetailResp ([a2cf072](https://github.com/wms-org/wms-admin/commit/a2cf072609ac33543605ecbb5f8e498237bc3d91)) +- 优化存储管理相关 API,合并 StorageResp 及 StorageDetailResp ([f7b5a4f](https://github.com/wms-org/wms-admin/commit/f7b5a4ff8dd93f444c00d103b0609ae81e0dd70c)) +- 优化字典管理相关 API ([9ec5945](https://github.com/wms-org/wms-admin/commit/9ec594509f2d4b31f46e3aca66d65d139dc8b94f)) +- 移除部门、角色、菜单、用户、存储的状态默认值 ([bd5ede2](https://github.com/wms-org/wms-admin/commit/bd5ede2e2956057376b930ecfed88ca44437cbc1)) +- 代码生成新增 MySQL json 数据类型映射 ([fe57350](https://github.com/wms-org/wms-admin/commit/fe5735090d94f5d900d79142e5e42ba2db9c0249)) +- 优化角色管理相关 API,角色编码不允许修改 ([df59cee](https://github.com/wms-org/wms-admin/commit/df59cee98565f9f45b04d16533888be39c3d7a6f)) +- 优化用户管理相关 API ([5269608](https://github.com/wms-org/wms-admin/commit/5269608c61b1f5a6a9f61cd45b349f28db714232)) +- 文件管理查询 API 调整为分页查询 ([f8bea90](https://github.com/wms-org/wms-admin/commit/f8bea901938aec0f0ac21c63179c5bde2a0965a7)) +- 移除 Qodana 扫描 ([d88581f](https://github.com/wms-org/wms-admin/commit/d88581f939afb0caa3589c8ee76c9b296bb9997e)) +- 移除菜单导出接口 ([4363c91](https://github.com/wms-org/wms-admin/commit/4363c91872e6d83e139b22c95a0d7e83183d8f69)) +- 优化系统日志、在线用户、存储管理、部门管理相关代码 ([a2e4f9a](https://github.com/wms-org/wms-admin/commit/a2e4f9a28b744e269c46dc66c60311bb939021a7)) +- 优化查询参数字典 API 地址 ([79a3de8](https://github.com/wms-org/wms-admin/commit/79a3de8971c613277bdcea79463b6f06959e7b85)) +- 移除角色状态字段 ([e89ba7d](https://github.com/wms-org/wms-admin/commit/e89ba7d5cd793e20c3562c7bd1e4655ed1e5a2a3)) + + +### 🐛 问题修复 + +- 使用字典时,仅查询启用状态字典 ([17c795f](https://github.com/wms-org/wms-admin/commit/17c795fedef5b6801f2053d97b9d78d067775ca1)) +- 获取 Authorization 请求头内容兼容小写请求头场景 ([e68c445](https://github.com/wms-org/wms-admin/commit/e68c4455a8af1b4d7a25cd63f9fc9e5aabb441ab)) +- 修复查询用户权限存在空值的问题 ([fce4a56](https://github.com/wms-org/wms-admin/commit/fce4a566d7204791650153f0a5507a5d05d2d6c3)) +- 存储管理 S3 存储功能修复 (GitHub#51) ([f71c4c2](https://github.com/wms-org/wms-admin/commit/f71c4c226ffd7c27f6726873be6af125affaf148)) +- 修复 sys_role_menu 表初始数据错误 ([70ed667](https://github.com/wms-org/wms-admin/commit/70ed667c16388093204eecd97e4914076c62d1ff)) +- 修复用户管理/角色管理编辑及状态变更问题 (GitHub#53) ([abf1e65](https://github.com/wms-org/wms-admin/commit/abf1e651e9782a6f7bf2a896018de17130038c57)) +- 修复Failed to submit a listener notification task. Event loop shut down? 问题,开发时表现为需要点击两次才能关闭程序 ([f5ab22e](https://github.com/wms-org/wms-admin/commit/f5ab22eedf594cee43592a2f29409ee9c33a88d3)) + +### 💥 破坏性变更 + +- 适配 wms-starter 2.0.0,top.charles7c.wms.starter => top.wms.starter ([f5ab22e](https://github.com/wms-org/wms-admin/commit/f5ab22eedf594cee43592a2f29409ee9c33a88d3)) +- 移除 monitor 模块 ([b6206a3](https://github.com/wms-org/wms-admin/commit/b6206a334671894306043f86ec07d7c045cd757d)) +- top.charles7c.wms.admin => top.wms.admin ([08eeabc](https://github.com/wms-org/wms-admin/commit/08eeabc47d58db3cfc861a3a527e52bf89f6183b)) +- 公告管理 Announcement => Notice ([dbe93df](https://github.com/wms-org/wms-admin/commit/dbe93df8bcec0b7dfb24fbd92f35928a3156f4e5)) + +### 📦 依赖升级 + +- ContiNew Starter 1.5.1 => 2.0.0 (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v2.5.0](https://github.com/wms-org/wms-admin/compare/v2.4.0...v2.5.0) (2024-03-23) + +### ✨ 新特性 + +* 新增 PostgreSQL 数据源配置示例 ([ee48c80](https://github.com/wms-org/wms-admin/commit/ee48c80cd10a4c4546d1cb24f1f4716bb2ac08ea)) +* 新增 PostgreSQL 部署脚本 ([3129e0a](https://github.com/wms-org/wms-admin/commit/3129e0a6dcbd809f0013fbf6c53ad029ae9f7a0e)) +* 新增 PostgreSQL 初始 SQL 脚本 ([33b8102](https://github.com/wms-org/wms-admin/commit/33b81029df0b51058b3525b4317b51a2351319dc)) +* 新增代码生成器插件模块(后续会改造为独立插件) ([87829d3](https://github.com/wms-org/wms-admin/commit/87829d3ce8ab5a35091800900f7d7708f15ed9c2)) +* 代码生成同步最新数据表结构支持同步排序 ([89546de](https://github.com/wms-org/wms-admin/commit/89546deced78f83daca7ac0ba2e7d3d8cd101d0c)) +* 新增代码批量生成功能 ([Gitee PR#12](https://gitee.com/wms/wms-admin/pulls/12)) ([040f137](https://github.com/wms-org/wms-admin/commit/040f137934130451700bc28aeabbced30970c5f6)) + +### 💎 功能优化 + +- 移除 ` 符号的使用,保持数据库无关性 ([d6b07bd](https://github.com/wms-org/wms-admin/commit/d6b07bd6d1b1f9077a7571702b58c5e9c782b446)) +- 优化字符串模板方法 API 使用 ([0f39384](https://github.com/wms-org/wms-admin/commit/0f393845a19432e7c965e811c96774694f4d2372)) +- 调整部分 SQL 语句,以兼容 PostgreSQL 数据库 ([9f5049b](https://github.com/wms-org/wms-admin/commit/9f5049bf26c557738867dfe833261d60d071d4a8)) ([bf60d48](https://github.com/wms-org/wms-admin/commit/bf60d48d3a53dd5d73a78e73b6b230e3271ec3de)) +- 新增插件仓库配置 ([0439252](https://github.com/wms-org/wms-admin/commit/04392524ac13c2096b549f99d0391fa1d375ca31)) +- 优化部分接口响应格式为 kv 格式 ([b40d872](https://github.com/wms-org/wms-admin/commit/b40d872bc4b8dd30ad952d639158619b43cef999)) +- 适配 Crane4j 条件注解 ([bf00747](https://github.com/wms-org/wms-admin/commit/bf007470b2362159309ff8231a2f0ad180cfc947)) +- 重构代码生成配置 ([7031a51](https://github.com/wms-org/wms-admin/commit/7031a51cd4d7072d4da841736678bb81b2123e9d)) +- 重构代码生成功能,由指定路径生成模式调整为下载模式,更方便复杂场景 ([df0c0dd](https://github.com/wms-org/wms-admin/commit/df0c0dd7dcf39620abaf21bd450620ec3fffcf37)) + + +### 🐛 问题修复 + +- 修复 MySQL 初始 SQL 脚本数据错误 ([49d6bd6](https://github.com/wms-org/wms-admin/commit/49d6bd6874b3df66fd2e2051ea273cb43cb7b4f6)) +- 修复参数缓存未及时过期的问题 ([976e9c4](https://github.com/wms-org/wms-admin/commit/976e9c43df5926c533723a75222c59fde05e122e)) +- 修复代码生成 text 类型数据的长度校验时,数值显示为 65,535 的问题 ([8026f66](https://github.com/wms-org/wms-admin/commit/8026f660c7af7bba6d4caaf31535a890e5b40a96)) + +### 💥 破坏性变更 + +- 调整 liquibase 目录结构,更适合开源类项目适配多种数据库脚本场景 ([1ca48a6](https://github.com/wms-org/wms-admin/commit/1ca48a6620cff62f3648cc28042843163589e150)) +- 适配 ContiNew Starter 日志及数据库工具的包结构优化 ([3405868](https://github.com/wms-org/wms-admin/commit/3405868c7f042beafb77a7407a388a40b9a75466)) +- 适配 ContiNew Starter Query 组件的包结构优化 ([6be1b6c](https://github.com/wms-org/wms-admin/commit/6be1b6cfb1e7fef4422b8c38e6073a435ebae5c2)) + +### 📦 依赖升级 + +- ContiNew Starter 1.4.0 => 1.5.1 (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v2.4.0](https://github.com/wms-org/wms-admin/compare/v2.3.0...v2.4.0) (2024-02-16) + +### ✨ 新特性 + +* 集成 TLog(轻量级的分布式日志标记追踪神器) ([Gitee PR#10](https://gitee.com/wms/wms-admin/pulls/10)) +* 系统日志新增 traceId 链路号记录,方便查看完整日志链路 ([860ca40](https://github.com/wms-org/wms-admin/commit/860ca403c2c32cc6395c1608217bc9b6e7c18bd8)) +* 取消用户默认密码,改为表单填写密码 ([3d77aa9](https://github.com/wms-org/wms-admin/commit/3d77aa91ee32065b53d9c47a57c33d6d7e4efb0e)) +* 适配 ContiNew Starter 加密模块(安全模块) ([6435175](https://github.com/wms-org/wms-admin/commit/6435175dc3d853cb170270e39e8f1505adffeae5)) ([43da462](https://github.com/wms-org/wms-admin/commit/43da462560e224ed92f239cb5af4db64dea51d18)) +* 适配 ContiNew Starter 脱敏模块(安全模块) ([2109789](https://github.com/wms-org/wms-admin/commit/2109789116d9ff18773d8afeb854d1dfc70b935a)) + +### 💎 功能优化 + +- 优化 API 文档分组配置 ([2df4cce](https://github.com/wms-org/wms-admin/commit/2df4cceedd35b1c2c07bcbf38b5a157604a752c2)) +- 优化 QueryTypeEnum 枚举值命名 ([9648cf6](https://github.com/wms-org/wms-admin/commit/9648cf64a4679657f0e609f980805d274563aa53)) +- 优化 Query 相关注解使用方式 ([15b1520](https://github.com/wms-org/wms-admin/commit/15b152008c6ae8ab89704d83a969dcfbbb8b5b88)) +- 新增 Qodana 扫描 ([f6a9581](https://github.com/wms-org/wms-admin/commit/f6a9581adef87a8915639e6cb2d7c4d02315ebd0)) +- 新增 SonarCloud 扫描 ([a154abd](https://github.com/wms-org/wms-admin/commit/a154abde8a39cfecc421c79e01998274b944d2c1)) ([c03c082](https://github.com/wms-org/wms-admin/commit/c03c082d2e2884962547633f5e98663088bd2c3b)) +- 移除 Lombok 私有构造注解使用 ([a2420d3](https://github.com/wms-org/wms-admin/commit/a2420d3f4b4652a1d9711f513b8fb22a56105141)) +- 获取不到当前登录用户信息则抛出未登录异常 ([d972a44](https://github.com/wms-org/wms-admin/commit/d972a4466a9e8a1a6e6375e4171a4790c2ba156e)) +- 优化代码,解决 [Sonar](https://sonarcloud.io/organizations/charles7c/projects)、[Codacy](https://app.codacy.com/gh/wms-org/wms-admin/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)、[Qodana](https://qodana.cloud/organizations/pQDPD/teams/p5jqd/) 扫描问题,点击各链接查看对应实时质量分析报告(Codacy 已达到 A) +- 优化部署配置 ([b5d668e](https://github.com/wms-org/wms-admin/commit/b5d668e014690d3f1a8a2bab0d0ad0039083e7bb)) +- 使用密码编码器重构密码加密、密码判断等相关处理 ([594f7fd](https://github.com/wms-org/wms-admin/commit/594f7fd042f1ff96a298f2e59ffdda112113cb51)) +- 优化 SaToken 及图形验证码配置 ([70973db](https://github.com/wms-org/wms-admin/commit/70973db71f2eed49c5878d69d8b93ff04b13a8b9)) +- 优化图形验证码使用及部分配置 ([a50d857](https://github.com/wms-org/wms-admin/commit/a50d857c41d164355d36ae5dfd14c6badbe06202)) + + +### 🐛 问题修复 + +- 修复 API 响应内容类型错误 ([439f7c7](https://github.com/wms-org/wms-admin/commit/439f7c7c58ee27ff56b5093df71bc902c46f48fa)) + +### 💥 破坏性变更 + +- 调整自增 ID 为分布式 ID ([4779887](https://github.com/wms-org/wms-admin/commit/4779887751bd3a696e4d31294057e8c03d66eaf3)) + +### 📦 依赖升级 + +- ContiNew Starter 1.2.0 => 1.4.0 (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v2.3.0](https://github.com/wms-org/wms-admin/compare/v2.2.0...v2.3.0) (2024-01-21) + +### ✨ 新特性 + +* 代码生成 Request 实体时,针对字符串类型增加数据长度校验注解 ([ee82558](https://github.com/wms-org/wms-admin/commit/ee8255876f618137f811e14ee305509e4e6466d0)) +* 适配 Crane4j 数据填充组件,优化部分数据填充处理 ([d598408](https://github.com/wms-org/wms-admin/commit/d5984087a306de31690e9a81d951bd831434a0c9)) ([a2411f7](https://github.com/wms-org/wms-admin/commit/a2411f728a811910a0918668c0811e7df0345640)) ([7a3ccc2](https://github.com/wms-org/wms-admin/commit/7a3ccc2dee8f7b938a91df52fd4903ce09e662e5)) +* 移除 Spring Cache,适配 JetCache ([d4bb39d](https://github.com/wms-org/wms-admin/commit/d4bb39d9b4483b7d7fed76b5f2b997538d86d719)) ([1b7aa9d](https://github.com/wms-org/wms-admin/commit/1b7aa9db0c56af733f63f602988dba9e225fe445)) ([8596e47](https://github.com/wms-org/wms-admin/commit/8596e47ed62e19083d6007448c5369d72fa4f2b6)) + +### 💎 功能优化 + +- 优化本地存储库注册 ([918e897](https://github.com/wms-org/wms-admin/commit/918e897838628b24a160c3a8f3b3dea1eefd1883)) +- 增加华为云镜像源仓库配置 ([16ee2b4](https://github.com/wms-org/wms-admin/commit/16ee2b4b6fe2a663830972ed99d4e80ddf5a3593)) +- 优化部分字段名称 ([e3e958b](https://github.com/wms-org/wms-admin/commit/e3e958b419e1ea23fe146b255fce749050302f63)) +- 调整代码生成前端 Vue 页面模板 ([7c34574](https://github.com/wms-org/wms-admin/commit/7c345745aadc6272e5f1db674c757ff0f9cea604)) +- 更新格式配置,优化全局代码格式 ([35e3123](https://github.com/wms-org/wms-admin/commit/35e31233c531b68b761c73fc0daf7444843b4059)) +- 优化配置文件格式 ([a8a4cad](https://github.com/wms-org/wms-admin/commit/a8a4cad840b6d32fbb8d3df24b193f9e7c826d22)) +- 使用钩子方法优化部分增、删、改处理 ([61c5724](https://github.com/wms-org/wms-admin/commit/61c57242fa481d2668b1d1ac4ff4802c47fd07bc)) +- 完善 flatten Maven 插件配置,以覆盖更多使用情况 ([657accd](https://github.com/wms-org/wms-admin/commit/657accd8a595ab0c2e9ff4d00e49c569eae03123)) +- 移除部分无用 Maven 配置 ([5db1f66](https://github.com/wms-org/wms-admin/commit/5db1f669e0bc5022bcd2164757a0f82dfe8d6c30)) +- 优化日志配置,滚动策略调整为基于日志文件大小和时间滚动 ([2fa8c25](https://github.com/wms-org/wms-admin/commit/2fa8c254fc53cda3d33c56931569822e645dd902)) + +### 🐛 问题修复 + +- 完善代码生成前端路径配置校验 ([bee04d5](https://github.com/wms-org/wms-admin/commit/bee04d5f363b6de88df5249b0fba85607978b303)) + +### 💥 破坏性变更 + +- 根据发展需要,拆分前端项目 wms-admin-ui 到独立仓库 ([4067eb9](https://github.com/wms-org/wms-admin/commit/4067eb97bf344dec6ae718433b57bdb7d0b8d6cd)) +- PageDataResp => PageResp ([d8c946e](https://github.com/wms-org/wms-admin/commit/d8c946e8014d205c4fd3f38d1f04b3225faede7a)) +- 适配 ContiNew Starter IService 接口,CRUD 查询详情方法不再检查是否存在 ([47a133a](https://github.com/wms-org/wms-admin/commit/47a133a065b5c858b588bf77ad51bb9fc38d1222)) +- 适配 ContiNew Starter CRUD 模块注解 ([7fa70e7](https://github.com/wms-org/wms-admin/commit/7fa70e74070c7c0f487baa5098f85d7dfb808106)) +- 调整部分类的所在包 ([8dc42c7](https://github.com/wms-org/wms-admin/commit/8dc42c7a21e7422399b49690b28899df299e20c7)) ([6efe1ad](https://github.com/wms-org/wms-admin/commit/6efe1ad6f416c52130b2380a699129e7dae29499)) + +### 📦 依赖升级 + +- ContiNew Starter 1.1.0 => 1.2.0 (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) + +## [v2.2.0](https://github.com/wms-org/wms-admin/compare/v2.1.0...v2.2.0) (2023-12-31) + +### ✨ 新特性 + +* 发送短信验证码新增限流处理 ([e719d20](https://github.com/wms-org/wms-admin/commit/e719d207fb76c82b584f2e1ac7210061dc71a89a)) +* 代码生成新增生成预览功能 ([4017029](https://github.com/wms-org/wms-admin/commit/401702972f30c4e556a2cf8d048f78fa9ee1c5ba)) ([505ba49](https://github.com/wms-org/wms-admin/commit/505ba49a5304fb3e2ba655dea901cd5e3ea74673)) +* 适配 ContiNew Starter 行为验证码,系统内所有短信发送新增前置行为验证码验证 ([Gitee PR#9](https://gitee.com/wms/wms-admin/pulls/9)) +* 文件管理:提供文件上传、下载、预览(目前支持图片、音视频)、重命名、切换视图(列表、网格)等功能 +* 存储库管理:提供文件存储库新增、编辑、删除、导出等功能 + +### 💎 功能优化 + +- 优化 API 文档配置 ([108f1c4](https://github.com/wms-org/wms-admin/commit/108f1c4ae7b855ac0bab2d3fe028270472a8be71)) +- 调整枚举配置值为大写 ([3ece42b](https://github.com/wms-org/wms-admin/commit/3ece42b94e071ece87e6b4616f7817bf851ba28f)) +- 优化由于 Mock 引起的导出报错提示 ([349899b](https://github.com/wms-org/wms-admin/commit/349899b4fc9572450ca31d9a5e19268ce0b868a8)) +- 优化查询访客地域分布信息接口 SQL ([4df887d](https://github.com/wms-org/wms-admin/commit/4df887d82678ced0d30aa0c7a6f92edcac902052)) +- 调整后端部分方法名 save => add ([45bd3e1](https://github.com/wms-org/wms-admin/commit/45bd3e10b6ac6aecde41ff9484668e557a485b27)) +- 优化系统日志详情 ([55effa3](https://github.com/wms-org/wms-admin/commit/55effa36580a57ddedb688e2ce30bec45c761224)) ([99997c1](https://github.com/wms-org/wms-admin/commit/99997c160eefc152a6f4e74bcd9c5ef6fc77a9c5)) +- 移除部分方法中仅有单个非读操作的事务处理 ([b85d692](https://github.com/wms-org/wms-admin/commit/b85d69298de1a6c48d15300bb9ff1b3ea569fdbd)) +- 优化编译配置 ([ed8bb57](https://github.com/wms-org/wms-admin/commit/ed8bb57fe24dfbe8f45b8f53370ebb79f1511268)) +- 优化配置文件格式 ([3399bc8](https://github.com/wms-org/wms-admin/commit/3399bc8dde0c8c8ac6d3e583ffbe299f7e6dd80b)) + +### 🐛 问题修复 + +- 修复代码生成相关错误 ([3fdc50d](https://github.com/wms-org/wms-admin/commit/3fdc50d78ec50a878cec2b35c7d5028e741c42d7)) +- 更新仪表盘帮助文档部分过期链接 ([ac42836](https://github.com/wms-org/wms-admin/commit/ac4283679a847ed372db28aae1ea05fd791651b8)) + +### 💥 破坏性变更 + +- 适配 ContiNew Starter QueryTypeEnum 命名变更 ([97c273f](https://github.com/wms-org/wms-admin/commit/97c273f99ecb038e041e3d39dbfacf326d49cc1b)) +- 适配 ContiNew Starter Log HttpTracePro(日志模块) ([9bf0150](https://github.com/wms-org/wms-admin/commit/9bf015059b96f41c29f05ecbf7612d611b3a98c3)) +- 适配 ContiNew Starter 全局异常处理器 ([4ed4ddd](https://github.com/wms-org/wms-admin/commit/4ed4ddd4f055cefe1f85482bd6b9ef760978691b)) +- 适配 ContiNew Starter 数据权限解决方案(数据访问模块-MyBatis Plus) ([0849426](https://github.com/wms-org/wms-admin/commit/084942630ab0e1846c1836b8dc4bf5b2c9a5b16e)) +- 调整 IBaseEnum 所属包 ([e6c6e1c](https://github.com/wms-org/wms-admin/commit/e6c6e1cb0e326c5f531ca5cb2e17a1e26efac7d9)) +- 重构原有文件上传接口并优化配置文件配置格式 ([5e37025](https://github.com/wms-org/wms-admin/commit/5e370254dd00deaab62438c5feb4de14192ad7e6)) + +### 📦 依赖升级 + +- ContiNew Starter 1.0.0 => 1.1.0 ([fc80921](https://github.com/wms-org/wms-admin/commit/fc80921c047862b424ca625317f4657667bc2c6b)) (更多依赖升级详情,请查看 ContiNew Starter [更新日志](https://github.com/wms-org/wms-starter/blob/dev/CHANGELOG.md)) +- Arco Design Vue 2.53.0 => 2.53.3 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- Vite 4.5.0 => 4.5.1 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- TypeScript 5.2.2 => 5.3.3 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- unplugin-vue-components 0.25.2 => 0.26.0 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- @kangc/v-md-editor 2.3.17 => 2.3.18 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- eslint 8.53.0 => 8.56.0 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- @vueuse/core 10.5.0 => 10.7.0 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- vue-i18n 9.6.5 => 9.8.0 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- vue-json-pretty 2.2.4 => 2.3.0 ([2720275](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb)) +- 由于篇幅限制,仅列出部分前端依赖升级情况,更多请查看 [提交记录](https://github.com/wms-org/wms-admin/commit/2720275b97334545dde71d548173bfcda7e660cb) + +## [v2.1.0](https://github.com/wms-org/wms-admin/compare/v2.0.0...v2.1.0) (2023-12-03) + +### 💎 功能优化 + +- 优化数据权限注解 ([bb59a78](https://github.com/wms-org/wms-admin/commit/bb59a78573bec521e8852f1c88ce6078fb14b14e)) +- 回退全局响应结果处理器 ([c7a4e32](https://github.com/wms-org/wms-admin/commit/c7a4e329945d8368a9b93a2488c059cf3333feba)) +- 优化字典 Controller CRUD 注解使用 ([8c1c4b0](https://github.com/wms-org/wms-admin/commit/8c1c4b014463d073e848e2f2abc33e089efa2abb)) +- 优化常量命名风格,XxxConsts => XxxConstants ([ec28705](https://github.com/wms-org/wms-admin/commit/ec28705b6ff6dd26ec3ef673fb3827259f1b9c41)) +- 移除 XML 文件头部的协议信息 ([b476956](https://github.com/wms-org/wms-admin/commit/b47695603afb0c19679c4100c1e3c23bc8007238)) +- 优化菜单标题校验 ([3dd81a1](https://github.com/wms-org/wms-admin/commit/3dd81a1192c4e340dad0b1bae5e29d1d7218fb25)) + +### 🐛 问题修复 + +- 修复 mock 被错误关闭的问题 ([a34070f](https://github.com/wms-org/wms-admin/commit/a34070ffed3044ad2bea604701b074665e7b4e42)) +- 修复保存生成配置校验失效的问题,并优化部分提示效果 ([c34e934](https://github.com/wms-org/wms-admin/commit/c34e934bb553d7f814d0fb5aa87eac0f565289b4)) + +### 💥 破坏性变更 + +- 项目包结构 top.charles7c.cnadmin => top.charles7c.wms.admin ([b86fe32](https://github.com/wms-org/wms-admin/commit/b86fe329d07317fed6a7d0b7015856de4b9e75d1)) +- 适配 ContiNew Starter 全局错误处理配置 ([b62095d](https://github.com/wms-org/wms-admin/commit/b62095d66e2318d35e4af07b128203b5a5e016f7)) +- 适配 ContiNew Starter CRUD(扩展模块) ([ce5a2ec](https://github.com/wms-org/wms-admin/commit/ce5a2ec9319b86e69a6bda67a886e1c96079ffc2)) +- 适配 ContiNew Starter Mail(消息模块) ([ce785dd](https://github.com/wms-org/wms-admin/commit/ce785ddce28733eeefbf970ed08b01e36e0abd4b)) +- 适配 ContiNew Starter Excel(文件处理模块) ([1311ae3](https://github.com/wms-org/wms-admin/commit/1311ae3603a26dc44dfffc5be86ea1ab81ff7958)) +- 适配 ContiNew Starter 认证模块-JustAuth ([7ad8d17](https://github.com/wms-org/wms-admin/commit/7ad8d1773a8e50e37a326b8f73f9ba38a3a7ff3a)) ([f28fbd1](https://github.com/wms-org/wms-admin/commit/f28fbd14fa83a82df49b07f16070e0ff3385b0ec)) +- 适配 ContiNew Starter 认证模块-SaToken ([86ca8f0](https://github.com/wms-org/wms-admin/commit/86ca8f094ff6d1c00b52c1406985bc00105b297f)) +- 适配 ContiNew Starter 图形验证码 ([8a11a02](https://github.com/wms-org/wms-admin/commit/8a11a020e04e271da7b700b5d73cf8475b50ee5c)) +- 适配 ContiNew Starter MyBatis Plus 自动配置 ([7306cd9](https://github.com/wms-org/wms-admin/commit/7306cd9d2f9492aa39e11b8f0dc8c7c11b534a3f)) +- 适配 ContiNew Starter Redisson 自动配置 ([a40e609](https://github.com/wms-org/wms-admin/commit/a40e609ea14acda840d2771f05ca9690d41236a1)) +- 适配 ContiNew Starter Jackson、API 文档(Knife4j:Spring Doc)自动配置 ([a86f3a5](https://github.com/wms-org/wms-admin/commit/a86f3a5047eda2f67cc9ad7721006d2db1fd710f)) +- 适配 ContiNew Starter 线程池自动配置 ([ec1daaf](https://github.com/wms-org/wms-admin/commit/ec1daaf0456296dbc3704ae700b9577001bdd5bb)) +- 引入 ContiNew Starter,适配跨域自动配置 ([2c4f511](https://github.com/wms-org/wms-admin/commit/2c4f5116c999b9316ab0bee4fa661338fea63c11)) +- 项目 group id top.charles7c => top.charles7c.wms ([3e23acb](https://github.com/wms-org/wms-admin/commit/3e23acb3e257d5813b858356aa926a96c906acf1)) + +## [v2.0.0](https://github.com/wms-org/wms-admin/compare/v1.3.1...v2.0.0) (2023-11-15) + +### 💎 功能优化 + +- 优化部分代码格式 ([2f87310](https://github.com/wms-org/wms-admin/commit/2f87310bc886af604a2667285a973ec6ae983430)) +- 优化 401 状态处理逻辑 ([c70e28a](https://github.com/wms-org/wms-admin/commit/c70e28a535c78214fe8d68a09824c786c457ef06)) +- 优化超时登录处理逻辑 ([d5da184](https://github.com/wms-org/wms-admin/commit/d5da1847e33e6cd7a0e5c3434335044167c1241c)) + +### 🐛 问题修复 + +- sms4j 3.0.3 => 3.0.4 ([23558d4](https://github.com/wms-org/wms-admin/commit/23558d45620a48ed82b32a5bdd2f948a4a37263d)) +- 发送消息增加事务处理 ([Gitee#7](https://gitee.com/wms/wms-admin/pulls/7)) ([1ca6f6c](https://github.com/wms-org/wms-admin/commit/1ca6f6c7e5f8a7c78f74df547f14517293241ac4)) +- 修复前端控制台 eslint 告警 ([Gitee#6](https://gitee.com/wms/wms-admin/pulls/6)) ([f4523d2](https://github.com/wms-org/wms-admin/commit/f4523d24817b4fee5c015eaba6b98fe99f350bba)) ([2304f28](https://github.com/wms-org/wms-admin/commit/2304f28a942fa8ea3e6d36fbebbe9346b0d3b741)) +- 修复仪表盘访问趋势区块 y 轴数值过大时无法展示的问题 ([fea6024](https://github.com/wms-org/wms-admin/commit/fea602439a3c9589bee078bfa9ff1e7efb378d71)) +- 修复控制台报错 Please use theme before using plugins ([98fbe05](https://github.com/wms-org/wms-admin/commit/98fbe0506c1cbe2f3c16347d9610ebfa5688b506)) +- 调整 Logback 配置,取消启动时打印 Logback 状态日志 ([1f7fef5](https://github.com/wms-org/wms-admin/commit/1f7fef5b31212e94652777be37bea4d4e02eb8c7)) + +### 💥 破坏性变更 + +- 优化部署相关脚本,mariadb => mysql ([5f4f0f1](https://github.com/wms-org/wms-admin/commit/5f4f0f1b21fe882dc51801d7c508c10b87d7af36)) +- 适配 Java 16 新特性 ([cf30443](https://github.com/wms-org/wms-admin/commit/cf3044312c8631a8c2b306e466e3d4d663d8eb6d)) +- 适配 Java 14 新特性 ([38f52aa](https://github.com/wms-org/wms-admin/commit/38f52aaafa22ebc958a22b7c38b084c655064fbc)) +- 适配 Java 11 新特性 ([5a5bd16](https://github.com/wms-org/wms-admin/commit/5a5bd1681e076ac6814d552da5415a8f154b93af)) +- 升级前端依赖 ([79fa2c8](https://github.com/wms-org/wms-admin/commit/79fa2c8abcf5f70f96ae7c6de35c47dbae76ee2d)) ([c44162d](https://github.com/wms-org/wms-admin/commit/c44162d431cb87cae251067fff9a5ae707aed9b3)) + - Arco Design Vue 2.52.0 => 2.53.0 + - Vue 3.3.4 => 3.3.7 + - Vite 3.2.7 => 4.5.0 + - vue-router 4.2.4 => 4.2.5 + - vue-i18n 9.5.0 => 9.6.5 + - vue-tsc 1.2.0 => 1.8.22 + - @vueuse/core 9.13.0 => 10.5.0 + - pinia 2.1.6 => 2.1.7 + - rollup 3.20.2 => 4.3.0 + - vue-cropper 1.0.9 => 1.1.1 + - crypto-js 4.1.1 => 4.2.0 + - vite-svg-loader 3.6.0 => 4.0.0 + - highlight.js 11.8.0 => 11.9.0 + - mitt 3.0.0 => 3.0.1 + - consola 2.15.3 => 3.2.3 + - prettier 2.8.7 => 3.0.3 + - less 4.1.3 => 4.2.0 + - eslint 8.48.0 => 8.53.0 + - stylelint 15.10.3 => 15.11.0 + - lint-staged 13.2.0 => 3.0.3 +- 升级后端依赖 ([dea160a](https://github.com/wms-org/wms-admin/commit/dea160a7b2d69e1b46edc936c9a697048bbb507a)) ([95c27ea](https://github.com/wms-org/wms-admin/commit/95c27ea323e015c915d352618158df830b4d1c05)) ([fa23287](https://github.com/wms-org/wms-admin/commit/fa232874aa88ab14fdc669e54a907e5ef05d2a7e)) ([8dbec9d](https://github.com/wms-org/wms-admin/commit/8dbec9d1a3bcb0f6d7ef4bbfb9715effd61b2025)) ([3bd56d8](https://github.com/wms-org/wms-admin/commit/3bd56d8a1ee274aac6d4ea57d61f6d470de0dc9c)) ([7b741d5](https://github.com/wms-org/wms-admin/commit/7b741d5f8c42d154c5b325326d0cc954fb566502)) + - Spring Boot 2.7.16 => 3.0.5 => 3.1.5 + - javax.* => jakarta.* + - ServletUtil => JakartaServletUtil(Hutool) + - 其他配置变更 + - JDK 8 => JDK 17 + - Sa-Token 1.36.0 => 1.37.0(适配 Spring Boot 3.x) + - MyBatis Plus 3.5.3.2 => 3.5.4(适配 Spring Boot 3.x) + - Dynamic Datasource 3.6.1 => 4.2.0(适配 Spring Boot 3.x) + - Redisson 3.20.1 => 3.24.3(适配 Spring Boot 3.x) + - Knife4j 适配 Spring Boot 3.x + - ip2region 2.7.15 => 3.1.5.1(适配 Spring Boot 3.x) + - spotless 2.30.0 => 2.40.0 + +## [v1.3.1](https://github.com/wms-org/wms-admin/compare/v1.3.0...v1.3.1) (2023-11-15) + +### 💎 功能优化 + +- 完善 Redis 部署配置 ([39969eb](https://github.com/wms-org/wms-admin/commit/39969ebf6173fc379dc3501e9204a344d1cf62cf)) +- 优化 401 状态处理逻辑 ([8820c1d](https://github.com/wms-org/wms-admin/commit/8820c1dfc858b9ef9df470e90dfe9ba4b1166e29)) +- 优化超时登录处理逻辑 ([712eedb](https://github.com/wms-org/wms-admin/commit/712eedba1be0ec371119745d4596cd35c2ce25d6)) +- 优化部分变量命名 ([f15494d](https://github.com/wms-org/wms-admin/commit/f15494d34823ded87efc396d98e2eb0108f74a3d)) + +### 🐛 问题修复 + +- sms4j 3.0.3 => 3.0.4 ([3fcdb54](https://github.com/wms-org/wms-admin/commit/3fcdb54442b380e76838478fa46e8dfb70a2759b)) +- 发送消息增加事务处理 ([5d159c6](https://github.com/wms-org/wms-admin/commit/5d159c6ab337a9432419d84cf246cff506500567)) +- 修复仪表盘访问趋势区块 y 轴数值过大时无法展示的问题 ([47a5746](https://github.com/wms-org/wms-admin/commit/47a5746794e552faf9c41fbcc21af091a878eb95)) +- 修复控制台报错 Please use theme before using plugins ([47a8160](https://github.com/wms-org/wms-admin/commit/47a8160d70862a5ee7284c165004cece2714a10f)) +- 修复 Swagger 分组接口缺失 ([b63d7d7](https://github.com/wms-org/wms-admin/commit/b63d7d725da5e9e9b2db9fd59bd140d64b50040c)) + +## [v1.3.0](https://github.com/wms-org/wms-admin/compare/v1.2.0...v1.3.0) (2023-11-04) + +### ✨ 新特性 + +* 消息管理:提供消息查看、标记已读、全部已读、删除等功能(适配对接导航栏站内信功能) +* 新增头像上传前裁剪功能 ([Gitee#5](https://gitee.com/wms/wms-admin/pulls/5)) ([cbc652d](https://gitee.com/wms/wms-admin/commit/cbc652de77200d29bcd42bb399c86c2e7df29c4d)) ([28f4791](https://gitee.com/wms/wms-admin/commit/28f4791833060469d132c4383665e81458f9c852)) +* 支持手机号登录(演示环境不开放) ([4d70bc8](https://github.com/wms-org/wms-admin/commit/4d70bc84db47c36c13d8e41e3a33e5a589483de8)) +* 支持邮箱登录 ([17b169e](https://github.com/wms-org/wms-admin/commit/17b169eb0ea2ded759b6bccb213c78bfb3425941)) +* 个人中心-安全设置,支持绑定、解绑三方账号 ([efe4557](https://github.com/wms-org/wms-admin/commit/efe455736c158e73bf0c6514c31bec5d83fe843b)) +* 支持第三方账号登录 ([05cb609](https://github.com/wms-org/wms-admin/commit/05cb60978017edbd14f1c7af83053f8a91800b5c)) + +### 💎 功能优化 + +- 新增接口文档菜单,演示环境开放接口文档 ([4a42336](https://github.com/wms-org/wms-admin/commit/4a4233647f2ea212b007f591aafc50380b15c099)) +- 项目配置增加是否为生产环境配置项 ([38deb95](https://github.com/wms-org/wms-admin/commit/38deb950ac7b2ed81f0e10816e943156aa076795)) +- 优化校验相关方法命名 ([f25de2d](https://github.com/wms-org/wms-admin/commit/f25de2d7f835a3fa75d59d3de0a014c37b3b32e1)) +- 新增全局响应结果处理器 ([Gitee#3](https://gitee.com/wms/wms-admin/pulls/3)) ([992a8fc](https://gitee.com/wms/wms-admin/commit/992a8fca173ea76722b388aca462cff8a1128803)) ([Gitee#4](https://gitee.com/wms/wms-admin/pulls/4)) ([a0b1afc](https://gitee.com/wms/wms-admin/commit/a0b1afc546657766cb6031794b98ccc2b6e4cb2d)) +- 优化部分代码格式及注释 ([3a176ac](https://github.com/wms-org/wms-admin/commit/3a176ac5efbda4aea1e883b29e68861bd352d642)) +- 重构登录页面 UI 以适配多维度认证、第三方登录等场景 ([d40d5b4](https://github.com/wms-org/wms-admin/commit/d40d5b4ae61d858fbee3ffa0606ebebb4282d9a2)) ([a5a4cd4](https://github.com/wms-org/wms-admin/commit/a5a4cd49646db3fa1108a8b917ef70c7757e81ad)) +- 升级前端依赖 ([698a725](https://github.com/wms-org/wms-admin/commit/698a7251b742e6b679694f21bfc174904dca8990)) + - Arco Design Vue 2.51.0 => 2.52.0 + - vue-i18n 9.2.2 => 9.5.0 + - dayjs 1.11.9 => 1.11.10 + +- 升级后端依赖 ([698a725](https://github.com/wms-org/wms-admin/commit/698a7251b742e6b679694f21bfc174904dca8990)) + - Spring Boot 2.7.15 => 2.7.16 + - Sa-Token 1.35.0.RC => 1.36.0 + - Hutool 5.8.20 => 5.8.22 + + +### 🐛 问题修复 + +- 开放前端项目IP访问 ([22a291d](https://github.com/wms-org/wms-admin/commit/22a291d4cf48e33dc2415e44b5d991b46451e7eb)) +- 修复获取验证码倒计时显示 ([2f2905e](https://github.com/wms-org/wms-admin/commit/2f2905efdc0baec2f2c38f686f72306394801ebf)) +- 用户邮箱信息增加脱敏处理 ([5bb35a1](https://github.com/wms-org/wms-admin/commit/5bb35a13d6b5801317a295eacc67d88b2c3e1682)) +- 修复重载校验方法定义及使用错误 ([a1ccc42](https://github.com/wms-org/wms-admin/commit/a1ccc421c440e5fef54e5d22b9bed26d2b16dda5)) +- 修复个人中心密码设置状态显示错误的问题 ([b04a228](https://github.com/wms-org/wms-admin/commit/b04a228a1a5bc0a575dd9e29e515285708b8ca85)) +- 修复登录后访问首页却跳转到登录页面的问题 ([Fixes #23](https://github.com/wms-org/wms-admin/issues/23)) ([7cf5e00](https://github.com/wms-org/wms-admin/commit/7cf5e0018c87720303f731317b5eb3cb7d127327)) +- 修复字典名称表单校验 ([#22](https://github.com/wms-org/wms-admin/pull/22)) ([c0ee2ea](https://github.com/wms-org/wms-admin/commit/c0ee2eac026d2d5a950a41b6f0a475b95b71d47a)) + +### 💥 破坏性变更 + +- 调整后端请求、响应参数模型命名风格 ([87f9056](https://github.com/wms-org/wms-admin/commit/87f90567dbd99f873aea1b85510c7b9939a2abb8)) +- 枚举接口 BaseEnum => IBaseEnum ([f5e8b09](https://github.com/wms-org/wms-admin/commit/f5e8b0943c6076c476b7d78bb623707740fb452f)) +- 优化前端登录模块 API 路径 ([43590bf](https://github.com/wms-org/wms-admin/commit/43590bf66e7e4873a85bdd416bd38b269f3af80e)) +- 优化后端部分参数模型命名 ([51f5528](https://github.com/wms-org/wms-admin/commit/51f552892ccb11ed594bf908069a1fd426324b69)) +- 优化个人中心路由地址 ([36d52d3](https://github.com/wms-org/wms-admin/commit/36d52d3e1522cd221cf3f03d76efd3e0eaf1b18f)) +- 还原前端 loginStore 命名,重命名为 userStore ([8d39493](https://github.com/wms-org/wms-admin/commit/8d394937cfc8418799215bd3659d26bed1f834c5)) + +## [v1.2.0](https://github.com/wms-org/wms-admin/compare/v1.1.2...v1.2.0) (2023-09-24) + +### ✨ 新特性 + +* 字典管理:提供对系统公用数据字典的维护,例如:公告类型,支持字典标签背景色和排序等配置 +* 系统配置:提供修改系统标题、Logo、favicon 等基础配置功能,以方便用户系统与其自身品牌形象保持一致 +* 完善仪表盘最近访问区块内容 ([36fda57](https://github.com/wms-org/wms-admin/commit/36fda57d499b0c3fb092a13f269bc9ffb7a26a9e)) +* 完善仪表盘访问趋势区块内容 ([a1c20af](https://github.com/wms-org/wms-admin/commit/a1c20afb1b9eb447f62bfd2e4f2996dfdf37c8ca)) ([1722133](https://github.com/wms-org/wms-admin/commit/1722133ac4872b40d6d47f65f359dea8a354b91a)) +* 完善仪表盘访客地域分布区块内容 ([dc1691f](https://github.com/wms-org/wms-admin/commit/dc1691f0195ef6c96aee36f50fc7e86cfcf651b9)) +* 完善仪表盘热门模块区块内容 ([83b2e2a](https://github.com/wms-org/wms-admin/commit/83b2e2a7c02d38c7041497e0ac5b3b0e78abac29)) +* 完善仪表盘总计区块内容 ([3440aa4](https://github.com/wms-org/wms-admin/commit/3440aa4faa23e267735f564476d8bccaf8c0208f)) +* 完善仪表盘快捷操作区块内容 ([0178fbb](https://github.com/wms-org/wms-admin/commit/0178fbb89a0e75729aa60443a812496bd5b19cb8)) + +### 💎 功能优化 + +- 前端表单重置优化 ([e947312](https://github.com/wms-org/wms-admin/commit/e947312f244d6af01f18b542ff7395440c68b089)) +- 优化登录和菜单加载相关提示 ([d080120](https://github.com/wms-org/wms-admin/commit/d080120d4228e77200d8f152397b0ebee413b089)) +- 完善前后端校验 ([90d825a](https://github.com/wms-org/wms-admin/commit/90d825a02fdc54e8685508a6fe4fb2d5f20e77f4)) ([8e506dc](https://github.com/wms-org/wms-admin/commit/8e506dc6e69529627a0aace6118f7310cc2f030a)) +- 优化枚举字典处理,增加颜色类型 ([1f73aa7](https://github.com/wms-org/wms-admin/commit/1f73aa732d101c7f7a58bc678e85d597d54d9770)) +- 公告类型适配字典数据 ([3a3a5d6](https://github.com/wms-org/wms-admin/commit/3a3a5d6b712f435d77ea04301afa0bdd8703567f)) +- 优化通用查询注解多字段模糊查询 ([3758107](https://github.com/wms-org/wms-admin/commit/375810772aa8cb928fb1f6820e781cb43f869e03)) +- 合并菜单管理图标和标题列 ([36d38ae](https://github.com/wms-org/wms-admin/commit/36d38aec1602f5ac6d2afbb5c5adf4d6e455ab97)) +- 封装 Spring Boot 默认错误处理 ([b874ca0](https://github.com/wms-org/wms-admin/commit/b874ca0782eb116bdedfc08023959a977f170a94)) +- 优化分页查询登录日志列表接口实现 ([566c9a1](https://github.com/wms-org/wms-admin/commit/566c9a122453980b585bd68442bb545073504a3d)) +- 更换登录页面 banner ([6f19660](https://github.com/wms-org/wms-admin/commit/6f19660cfbc3be6e0d702e3f488e266c50622f0a)) +- 优化登录用户信息角色相关信息命名 ([be394f3](https://github.com/wms-org/wms-admin/commit/be394f3de4ea7ea692042db3556f706a3d141b51)) ([31f0abb](https://github.com/wms-org/wms-admin/commit/31f0abbae2e38d1cfa3f6221c9be0b54cf5337ad)) +- 升级前端依赖 ([c665902](https://github.com/wms-org/wms-admin/commit/c6659020f8bac7319c5c407389cd745527a8cd97)) +- 升级后端依赖 ([5049e1e](https://github.com/wms-org/wms-admin/commit/5049e1e312ab500e284abccbbee4186db2710d01)) ([d20aadf](https://github.com/wms-org/wms-admin/commit/d20aadfc93b54339d19d173fce364310e90b016d)) ([32904b5](https://github.com/wms-org/wms-admin/commit/32904b54ef63536ef5c5106adc00a7376b907632)) + +### 🐛 问题修复 + +- 修复删除列表数据后 Select 选择框重置问题 ([#21](https://github.com/wms-org/wms-admin/pull/21)) ([3288f2d](https://github.com/wms-org/wms-admin/commit/3288f2d38dfebc1381842d67cdfb17675c786859)) +- 修复前端部分拼写错误 ([62021f8](https://github.com/wms-org/wms-admin/commit/62021f8fdc171ad04d07c25c5a9357a64cc4a087)) + +### 💥 破坏性变更 + +- 优化系统内置类型数据标识 ([8a02401](https://github.com/wms-org/wms-admin/commit/8a02401a24b546f2a6aab04cf05371ecb4236ca0)) +- 分离 HTTP 状态码和业务状态码 ([b3b6446](https://github.com/wms-org/wms-admin/commit/b3b6446433972422cf62dfc47c031134b91cd7ec)) +- 调整生产环境本地存储、日志位置 ([2254e55](https://github.com/wms-org/wms-admin/commit/2254e555af9cade4897d5335b252a0312d6805eb)) +- 调整项目打包结构,分离依赖、配置文件 ([e679abf](https://github.com/wms-org/wms-admin/commit/e679abfccc6c80198512958b6d07b363074d9d76)) + +## [v1.1.2](https://github.com/wms-org/wms-admin/compare/v1.1.1...v1.1.2) (2023-09-24) + +### 💎 功能优化 + +- 优化后端程序启动成功输出内容 ([6322859](https://github.com/wms-org/wms-admin/commit/63228598d9fcd6e5d00172c12418a371d4c96766)) +- 配置子级菜单图标 ([5544836](https://github.com/wms-org/wms-admin/commit/55448364a39085debb776463f5e95a15b186c447)) + +### 🐛 问题修复 + +- 修复生产环境和开发环境样式不一致的问题 ([be8732d](https://github.com/wms-org/wms-admin/commit/be8732d812e021631864b0ff6225b4da24cafcee)) +- 排除路径配置放开 /error ([0428fe7](https://github.com/wms-org/wms-admin/commit/0428fe776224afb64601901cef4d3100e5d30bd6)) +- 修复初始数据缺失字段列表的问题 ([d5138e1](https://github.com/wms-org/wms-admin/commit/d5138e1e43bdc8b347e061890131ac2646b2dd3c)) +- 修复系统日志表索引缺失导致查询耗时较长的问题 ([ac43833](https://github.com/wms-org/wms-admin/commit/ac438337219f5a160d49b255805774da36ab865c)) +- 修复部分菜单数据 component 信息配置错误 ([11ea072](https://github.com/wms-org/wms-admin/commit/11ea072d600f24fe97fe8145208e821712b84839)) +- 修复图标 SVG 内容格式错误 ([20f1e8a](https://github.com/wms-org/wms-admin/commit/20f1e8aecc737b28ab869d363957513d868b4ab7)) + +## [v1.1.1](https://github.com/wms-org/wms-admin/compare/v1.1.0...v1.1.1) (2023-09-06) + +### 💎 功能优化 + +- 调整 Mock 响应时长,以解决前端偶发需重复登录问题 ([df19c5d](https://github.com/wms-org/wms-admin/commit/df19c5d2197fabb61cbdd4dccf1c427fb23d77d4)) + +### 🐛 问题修复 + +- 还原登录 Helper 优化(导致重大登录问题及查询在线用户错误) ([#15](https://github.com/wms-org/wms-admin/pull/15)) ([7a6db2d](https://github.com/wms-org/wms-admin/commit/7a6db2d14e60a5fcc1a2786e6eaa3d46a0714e6c)) ([#9](https://github.com/wms-org/wms-admin/pull/9)) ([9e2a5ef](https://github.com/wms-org/wms-admin/commit/9e2a5ef1249fd93dd10f2c255bf77c3eaa64a241)) +- 修复刷新页面后,选中菜单无法保持展开状态的问题 ([3fc7adb](https://github.com/wms-org/wms-admin/commit/3fc7adb1e2bd4b648753bd2999df725417e01680)) +- 修复侧边栏菜单无法显示自定义图标的问题 ([10ca5d8](https://github.com/wms-org/wms-admin/commit/10ca5d8c76aa39a207ea7db4442bf63ff4578273)) +- 更正 README 文档项目结构部分内容 ([486da2f](https://github.com/wms-org/wms-admin/commit/486da2f79bfc5379213bf666b8f325fb8096ebc6)) +- 修复公告缺失待发布状态的问题 ([#14](https://github.com/wms-org/wms-admin/pull/14)) ([46cc4c9](https://github.com/wms-org/wms-admin/commit/46cc4c9307e3cc7060ae436f59f007831104884a)) + +## [v1.1.0](https://github.com/wms-org/wms-admin/compare/v1.0.1...v1.1.0) (2023-09-01) + +### ✨ 新特性 + +* 公告管理:提供公告的发布、查看和删除等功能。管理员可以在后台发布公告,并可以设置公告的生效时间、终止时间,以 markdown-it 为内核渲染 Markdown 格式内容显示 +* 代码生成:提供根据数据库表自动生成相应的前后端 CRUD 代码的功能 +* 允许表格调整列宽,不允许新增/修改类表单对话框按 Esc 关闭 ([1b06a96](https://github.com/wms-org/wms-admin/commit/1b06a96cfbe5774931d8c4c0d7827703caa096df)) + +### 💎 功能优化 + +- 最终适配及启用 Arco Design Pro Vue 动态路由 ([9baf341](https://github.com/wms-org/wms-admin/commit/9baf3410138cb8a152ec51f70340d500fa009510)) +- 优化分页总记录数数据类型 ([bfea689](https://github.com/wms-org/wms-admin/commit/bfea689b0eaf44c8d54b4fd59c042d72ac71e395)) +- 修复在线用户列表等自定义分页查询 NPE 的问题 ([015ff55](https://github.com/wms-org/wms-admin/commit/015ff5512b3662efce88d02ab1dda6d55501a501)) +- 对获取路由信息接口增加缓存处理 ([4639d13](https://github.com/wms-org/wms-admin/commit/4639d13ba61abfaed3c9d3da0e057892577b5c40))⚡ +- 完善前端 axios 请求响应拦截器 ([bb398d8](https://github.com/wms-org/wms-admin/commit/bb398d8101e3780f450c6508852fc727fb936cee)) ([e18692f](https://github.com/wms-org/wms-admin/commit/e18692fa74e0a0d9558db6643b945c6c6a00db36)) +- 优化仪表盘公告区块、帮助文档区块内容 ([b59a819](https://github.com/wms-org/wms-admin/commit/b59a819ad5f2bdbd357951f070d155e91f2d7903)) ([315c059](https://github.com/wms-org/wms-admin/commit/315c059713833be10b0cf05d302259a3146f3707)) ([6d024a9](https://github.com/wms-org/wms-admin/commit/6d024a90d7a231439c8e260b9bd625e8b5027515)) +- 将 Swagger 文档中的额外请求参数隐藏 ([#11](https://github.com/wms-org/wms-admin/pull/11)) ([a9ed02b](https://github.com/wms-org/wms-admin/commit/a9ed02bf4ff6a8a4d9f68db2d62d29000c543943)) +- 优化前端 CRUD 相关命名 ([6d81928](https://github.com/wms-org/wms-admin/commit/6d81928541f4da568e9c7138f91d4dc1c5c6dd4e)) +- 优化部分超链接标签属性 ([46a75d0](https://github.com/wms-org/wms-admin/commit/46a75d029798e8d5a162b53b8a61c8e3c3f4dd9e)) +- 使用属性变量消除配置文件中分散的 ContiNew Admin 品牌元素 ([54ea410](https://github.com/wms-org/wms-admin/commit/54ea41048abd096cf1e2c32ee871c1eb85d4ece1)) +- 拆分 Swagger 接口文档分组 ([#10](https://github.com/wms-org/wms-admin/pull/10)) ([72df45e](https://github.com/wms-org/wms-admin/commit/72df45e9b3373d28f1845af16a81cb8bd8408647)) +- 优化登录 Helper ([#9](https://github.com/wms-org/wms-admin/pull/9)) ([9e2a5ef](https://github.com/wms-org/wms-admin/commit/9e2a5ef1249fd93dd10f2c255bf77c3eaa64a241)) +- 将全局异常处理器未知异常的异常类型从 Exception 调整为 Throwable ([90e1c64](https://github.com/wms-org/wms-admin/commit/90e1c64db684df97454e4753932b7f4017d8e23d)) +- 优化 == 及 != 表达式格式 ([487fa82](https://github.com/wms-org/wms-admin/commit/487fa82306fbd84033f6c39ad20b72755b03e875)) +- 集成 Spring Cache,优化查询用户昵称性能 ([b23b00d](https://github.com/wms-org/wms-admin/commit/b23b00d02a4738a61b4a13676fab6d2c9ec927de)) ([76622c2](https://github.com/wms-org/wms-admin/commit/76622c238f1d6028826407490e50a14bdba25ade))⚡ +- 将验证码唯一标识格式从无符号 UUID 调整为带符号 UUID ([a61196c](https://github.com/wms-org/wms-admin/commit/a61196cd62cea4f684154bb42a949656650f626b)) +- 完善接口文档示例信息 ([#7](https://github.com/wms-org/wms-admin/pull/7)) ([ad7d699](https://github.com/wms-org/wms-admin/commit/ad7d6995ba40a0cb70a194693fa450bdbb3cc7a0)) ([#8](https://github.com/wms-org/wms-admin/pull/8)) ([0ac0213](https://github.com/wms-org/wms-admin/commit/0ac0213628023c04b5be531522d76f09712f7317)) ([190385e](https://github.com/wms-org/wms-admin/commit/190385ed3636206224bc90780fcede2e49f9c118)) ([332bd6c](https://github.com/wms-org/wms-admin/commit/332bd6cd2a9b4e25678a3eec565965c5b2702aa2)) +- 使用 DatePattern 中的日期格式常量替代字符串常量中的日期格式 ([241a9cf](https://github.com/wms-org/wms-admin/commit/241a9cf85b3c19eb093d4d661c35d71c490adf1f)) +- 优化分组校验 ([78a5d5e](https://github.com/wms-org/wms-admin/commit/78a5d5ec7a14ee37d92a9520211adca23f12b287)) +- 优化 springdoc-openapi 对象型参数处理 ([ae8d294](https://github.com/wms-org/wms-admin/commit/ae8d294705536e99d6c30a9ff5257fdb3ee5b35f)) +- 升级前端依赖,并更换包管理器 yarn => pnpm ([6164110](https://github.com/wms-org/wms-admin/commit/6164110462cc3aff66d79539f54e84d47c6d5894)) +- 升级后端依赖 ([51a82d8](https://github.com/wms-org/wms-admin/commit/51a82d8f4eabd6aa27e1a991f05f516171b6ae03)) + +### 🐛 问题修复 + +- 完善部分数据库表的唯一索引 ([88d6118](https://github.com/wms-org/wms-admin/commit/88d6118693586fbd8da573df3b2f942d049e4b3c)) +- 修复访问 doc.html 接口文档,控制台报 No mapping for GET /favicon.ico 警告的问题 ([94f88ba](https://github.com/wms-org/wms-admin/commit/94f88bad2278d64a4b8a3bc930a9f754fb00cba6)) +- 登录页面输入错误时,自动清空验证码输入框 ([a76f47f](https://github.com/wms-org/wms-admin/commit/a76f47fbd86bfa7fbf85440c653ae6259fce7969)) + +### 💥 破坏性变更 + +- 更新信息调整为仅在更新数据时自动填充 ([df77e57](https://github.com/wms-org/wms-admin/commit/df77e574cca605afd89f1b3781f1cde699bcb7e6)) +- 将时间戳单位从毫秒调整为秒 ([fa916b9](https://github.com/wms-org/wms-admin/commit/fa916b93247e10462eb44185ad45cdca4dedda7d)) +- 移除所有的 @Accessors(chain = true),并全局配置禁止使用 ([76c6546](https://github.com/wms-org/wms-admin/commit/76c65463c2e5ddf0c90fa1622fd86706a4373c80)) + +## [v1.0.1](https://github.com/wms-org/wms-admin/compare/v1.0.0...v1.0.1) (2023-08-17) + +### 💎 功能优化 + +- 优化根据 ID 查询用户昵称方法 ([4a8af1f](https://github.com/wms-org/wms-admin/commit/4a8af1f72d9249afa1c013e08674f492f453b020)) +- 优化 BaseController 中部分权限码的使用 ([b0b1127](https://github.com/wms-org/wms-admin/commit/b0b1127b5bd39e9bc431e9fa9c86201bbc18e891)) +- 优化分页总记录数数据类型 ([76f04dd](https://github.com/wms-org/wms-admin/commit/76f04dd38f90aad6abf82d2dccba031d4d9108cf)) +- 优化通用查询注解解析器 ([a623acd](https://github.com/wms-org/wms-admin/commit/a623acd4a5529ae42898ec359f595716acc5bab8)) ([b632c18](https://github.com/wms-org/wms-admin/commit/b632c183994ac71382180a38bf7bdb7a6315c1e6)) +- 优化数据库表结构中部分类型长度 ([f3fabea](https://github.com/wms-org/wms-admin/commit/f3fabea7dd736d94badecbc08091eec6274f5fb7)) +- 使用常量优化部分魔法值 ([e6f7429](https://github.com/wms-org/wms-admin/commit/e6f7429fa30cbc87c03a073a53b6f7df24d33d8d)) +- 优化部分 Properties 用法 ([48de2e8](https://github.com/wms-org/wms-admin/commit/48de2e85e0fbf60f10769cd3529f79ac3c531e92)) + +### 🐛 问题修复 + +- 修复获取字典参数为空时的判断条件 ([#6](https://github.com/wms-org/wms-admin/pull/6)) ([104f69e](https://github.com/wms-org/wms-admin/commit/104f69e8a09ce36163f6f9680b2d8d61bb45f11a)) +- 完善查询用户数据权限 ([026247f](https://github.com/wms-org/wms-admin/commit/026247f677110ae199124a67c68503729cbaec92)) +- 解决 IDE 报 Delete ␍ eslint(prettier/prettier) 警告的问题 ([8743ed1](https://github.com/wms-org/wms-admin/commit/8743ed14d927ab52814ed5f5f166afaa7a6b78b2)) +- 修复分页查询条件默认值未生效的问题 ([2d2a7e7](https://github.com/wms-org/wms-admin/commit/2d2a7e7c8e31763ac3ea514d8a92c3938376dd3a)) +- 完善各模块事务注解 ([18c54a7](https://github.com/wms-org/wms-admin/commit/18c54a74fc6ff0650ff53eeadc094d7e1df0b0a5)) +- 修复邮箱健康检查报错问题并优化部分配置写法 ([5968f40](https://github.com/wms-org/wms-admin/commit/5968f402ed478244d36f5825373190ed00d8c1f1)) +- 完善各模块参数校验 ([8b955a0](https://github.com/wms-org/wms-admin/commit/8b955a0b1bde4e8959fc0dfbc11a326d9eec0b45)) + +## v1.0.0 (2023-03-26) + +### ✨ 新特性 + +* 用户管理:提供用户的相关配置,新增用户后,默认密码为 123456 +* 角色管理:对权限与菜单进行分配,可根据部门设置角色的数据权限 +* 部门管理:可配置系统组织架构,树形表格展示 +* 菜单管理:已实现菜单动态路由,后端可配置化,支持多级菜单 +* 在线用户:管理当前登录用户,可一键踢下线 +* 日志管理:提供在线用户监控、登录日志监控、操作日志监控和系统日志监控等监控功能 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f50c997 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2022-present Charles7c Authors. 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8eb2c32 --- /dev/null +++ b/README.md @@ -0,0 +1,507 @@ +# ContiNew Admin 中后台管理框架 + + +Release + + +ContiNew Starter + + +Spring Boot + + +Open JDK + + +Codacy + + +Sonar + +
+ +License + + +GitHub Stars + + +GitHub Forks + + +Gitee Stars + + +Gitee Forks + + +GitCode Stars + + +📚 [在线文档](https://wms.top) | 🚀 [演示地址](https://admin.wms.top)(账号/密码:admin/admin123) + +## 简介 + +ContiNew Admin(Continue New Admin)持续迭代优化的前后端分离中后台管理系统框架。开箱即用,重视每一处代码规范,重视每一种解决方案细节,持续提供舒适的前、后端开发体验。 + +当前采用的技术栈:Spring Boot3(Java17)、Vue3 & Arco Design & TS & Vite、Sa-Token、MyBatis Plus、Redisson、JetCache、JustAuth、Crane4j、EasyExcel、Liquibase、Hutool 等。 + +## 项目源码 + +| | 后端 | 前端 | +| :------ | :----------------------------------------------------------- | :----------------------------------------------------------- | +| Gitee | [wms/wms-admin](https://gitee.com/wms/wms-admin) | [wms/wms-admin-ui](https://gitee.com/wms/wms-admin-ui) | +| GitCode | [wms/wms-admin](https://gitcode.com/wms/wms-admin) | [wms/wms-admin-ui](https://gitcode.com/wms/wms-admin-ui) | +| GitHub | [wms-org/wms-admin](https://github.com/wms-org/wms-admin) | [wms-org/wms-admin-ui](https://github.com/wms-org/wms-admin-ui) | + +## 项目起源 + +我热衷于做数据归档,归档后的数据可以提高学习/工作效率,为记忆“减负”,在持续的数据归档中,优质的“沉淀”会带来非匀速、跨越式的学习/工作体验。**数据归档是一件需要持续去做的事情**。 + +从接触程序代码的第一天,我的程序数据归档也随之开始了,刷过的算法题、笔记、对接各种组件的配置文件,甚至于一些亮眼的样式设计、“如诗”的代码片段。这些数据的沉淀丰富了我的解决方案,提高了我的编程效率,逐渐为各种场景落实成了一个个雏形程序。再后来,我意识到,我归档的这些雏形程序,有一个更为妥贴的名称:**程序框架/脚手架**。 + +技术的发展,导致这些雏形程序的生命周期很是短暂,它们有别于我归档的其他数据,有时由于工作的原因,没有时间很好的去沉淀它们,在使用时变得越来越不顺手。所以,某段时间,我放弃了维护,而是去采用一些更为成熟的框架。 + +不过,在陆续几年使用了一些成熟框架后,我前后遇到了一些困难: + +1. 代码洁癖想要找到一个**扩展性佳,代码规范良好,开发体验舒适**的框架很不容易,总是差些什么 +2. 项目上手困难或是基础版功能不全,需要的全在专业版,亦或者代码阅读性差,文档收费 +3. 部分解决方案缺失,已有解决方案也过于偏向样板化,无法形成良好的逻辑闭环 +4. 好不容易找到一些相较合适的,没过多久,部分作者可能暂时没法对外发“电”了,随着了解深入,很多 Bug 或新技术趋势还是需要自己研究解决 + +在工作中,很多想法/设计受限于客户需求、开发工期,必须优先以交付为导向,但一些优秀的实践需要花时间持续进行沉淀,只要我没跳出这个圈子,我还是需要一直去做好程序归档。“种一棵树最好的时间是十年前,其次是现在”,最终,我选择在业余时间更加正视这件事,从头归档沉淀,从添加每一个依赖开始,我希望它能持续的迭代优化、演进,所以我把它命名为 **ContiNew(Continue New)**。并且这次我选择了开源,我希望它不仅仅能吸收我的需求和沉淀,而是依托开源协作模式,及时发现更多的问题,接受更多的可能性,沉淀更优秀的思考,设计。 + +## 为什么选我们? + +> [!TIP] +> 更为完整的图文描述请查阅[《在线文档》](https://wms.top/admin/intro/why.html)。 + +1.**甄选技术栈:** ContiNew(Continue New) 项目致力于持续迭代优化,让技术不掉队。在技术选型时,进行深度广泛地调研,从流行度、成熟度和发展潜力等多方面甄选技术栈。 + +2.**Starter 组件:** 从 v2.1.0 版本开始,抽取并封装后端基础组件及各框架集成配置到 ContiNew Starter 项目,且 **[已发布至 Maven 中央仓库](https://central.sonatype.com/search?q=wms-starter&namespace=top.wms)**,可在你的任意项目中直接引入所需依赖使用。即使你不用脚手架项目,难道能让你搭项目框架更快、更爽、更省力的 Starter 也要 Say No 吗? + +3.**CRUD 套件:** 封装通用增删改查套件,适配后端各分层,几分钟即可提供一套 CRUD API,包括新增、修改、批量删除、查询详情、分页列表查询、全部列表查询、树型列表查询、导出到 Excel,且 API 支持按实际所需开放或扩展。 + +```java +@Tag(name = "部门管理 API") +@RestController +@CrudRequestMapping(value = "/system/dept", api = {Api.TREE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) +public class DeptController extends BaseController {} +``` + +4.**代码生成器:** 提供代码生成器,已配套前、后端代码生成模板,数据表设计完之后,简单配置一下即可生成前、后端 80% 的代码,包含 CRUD API、权限控制、参数校验、接口文档等内容。如果业务不复杂,也可能就是 95% 的代码。 + +5.**改善开发体验:** 持续优化及适配能改善开发体验的组件。 + +- 适配 ContiNew Starter 各组件,针对 Spring 基础配置、通用解决方案以及流行框架进行了深度封装的 starter 集合,改善你在开发每个 Spring Boot Web 项目的体验。(枚举参数处理、默认线程池、跨域、加密、脱敏、限流、日志、异常及响应通用解决方案等等,更多细节可查看 Starter 源码) +- 适配 Crane4j 数据填充组件,减少因为一个用户名而产生的联表回填; +- 适配 P6Spy SQL 性能分析组件,开发期间方便监控 SQL 执行; +- 适配 TLog 链路追踪组件,方便在杂乱的日志文件中追踪你某次请求的日志记录; +- 适配 JetCache 缓存框架(比 Spring Cache 更强大易用),通过注解声明即可快速实现方法级缓存,极大改善编码式缓存体验,且支持灵活的二级缓存配置、分布式自动刷新等能力; +- 前端适配 Vue Devtools(Vue 官方提供的调试浏览器插件),极大提高 Vue 开发及调试效率 + +6.**Almost最佳后端规范:** 后端严格遵循阿里巴巴 Java 编码规范,注释覆盖率 > 45%,接口参数示例 100%,代码分层使用体验佳,变量、方法命名清晰统一,前端代码也使用严格的 ESLint、StyleLint 等检查。良好的设计,代码复用率极高!写代码时,让你有一种无需多写,理应如此的感觉。我是代码洁癖,我实际写的时候很清楚这到底是不是乱吹。 + +7.**卓越工程:** 后端采用模块化工程结构,并适配了统一项目版本号、编译项目自动代码格式化、代码混淆等插件,提供了自定义打包部署结构配置(配置文件、三方依赖和主程序分离),提供全套环境及应用的 Docker Compose 部署脚本。为了减少您开发新项目时的改造耗时,项目品牌配置持续进行深度聚合,简单的配置和结构修改即可快速开始独属于你的新项目。 + +8.**业务脚手架:** 有颜有料,不止是说说而已,持续打磨 UI 设计与色彩主题。提供基于 RBAC 的权限控制、通用数据权限,包含丰富的通用业务功能:第三方登录,邮箱、短信(生产级炸弹漏洞处理方案),个人中心、用户管理、角色管理、部门管理、系统配置(基础站点配置、邮件配置、安全配置)、系统日志、消息中心、通知公告等,设计用心,逻辑合理闭环。 + +> 一个好的脚手架项目,不仅仅是提供一系列组件集成与配置,也不仅仅是封装一堆好用的工具,还更应该提供一系列通用基础业务解决方案及设计,为初创团队项目减负。 + +9.**质量与安全:** CI 已集成 Sonar、Codacy,Push 即扫描代码质量,定期扫描 CVE 漏洞,及时解决潜在问题。封装数据库字段加密、JSON 脱敏、XSS 过滤等工具,提供诸多安全解决方案。 + +由于篇幅有限,且项目正处于高速发展期,更多功能正在陆续上线(敬请关注仓库或群内动态)。另外像最基本的统一异常、错误处理,基础线程池等配置就不在此赘述,细节优化详情请 clone 代码查看。 +> Talk is cheap, show the code. + +## 系统功能 + +> [!TIP] +> 更多功能和优化正在赶来💦,最新项目计划、进展请进群或关注 [需求墙](https://wms.top/require.html) 和 [更新日志](https://wms.top/admin/other/changelog.html)。 + +- 仪表盘:提供工作台、分析页,工作台提供功能快捷导航入口、最新公告、动态;分析页提供全面数据可视化能力 +- 个人中心:支持基础信息修改、密码修改、邮箱绑定、手机号绑定(并提供行为验证码、短信限流等安全处理)、第三方账号绑定/解绑、头像裁剪上传 +- 消息中心:提供站内信消息统一查看、标记已读、全部已读、删除等功能(目前仅支持系统通知消息) +- 用户管理:管理系统用户,包含新增、修改、删除、导入、导出、重置密码、分配角色等功能 + +- 角色管理:管理系统用户的功能权限及数据权限,包含新增、修改、删除、分配角色等功能 + +- 菜单管理:管理系统菜单及按钮权限,支持多级菜单,动态路由,包含新增、修改、删除等功能 + +- 部门管理:管理系统组织架构,包含新增、修改、删除、导出等功能,以树形列表进行展示 + +- 字典管理:管理系统公用数据字典,例如:消息类型。支持字典标签背景色和排序等配置 + +- 通知公告:管理系统公告,支持设置公告的生效时间、终止时间、通知范围(所有人、指定用户) + +- 文件管理:管理系统文件,支持上传、下载、预览(目前支持图片、音视频、PDF、Word、Excel、PPT)、重命名、切换视图(列表、网格)等功能 + +- 存储管理:管理文件存储配置,支持本地存储、兼容 S3 协议对象存储 + +- 终端管理:多端认证管理,可设置不同的 token 有效期 + +- 系统配置: + - 基础配置:提供修改系统标题、Logo、favicon、版权信息等基础配置功能,以方便用户系统与其自身品牌形象保持一致 + - 邮件配置:提供系统发件箱配置,也支持通过配置文件指定 + - 安全配置:提供密码策略修改,支持丰富的密码策略设定,包括但不限于 `密码有效期`、`密码重复次数`、`密码错误锁定账号次数、时间` 等 + +- 在线用户:管理当前登录用户,可一键踢除下线 + +- 日志管理:管理系统登录日志、操作日志,支持查看日志详情,包含请求头、响应头等报文信息 + +- 任务管理:管理系统定时任务,包含新增、修改、删除、执行功能,支持 Cron(可配置式生成 Cron 表达式) 和固定频率 + +- 任务日志:管理定时任务执行日志,包含停止、重试指定批次,查询集群各节点的详细输出日志等功能 + +- 应用管理:管理第三方系统应用 AK、SK,包含新增、修改、删除、查看密钥、重置密钥等功能,支持设置密钥有效期 + +- 代码生成:提供根据数据库表自动生成相应的前后端 CRUD 代码的功能,支持同步最新表结构及代码生成预览 + +## 系统截图 + +> [!TIP] +> 受篇幅长度及功能更新频率影响,下方仅为系统 **部分** 功能于 **2024年11月18日** 进行的截图,更多新增功能及细节请登录演示环境或 clone 代码到本地启动查看。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
登录页面登录页面-H5
仪表盘分析页
个人中心消息中心
安全设置-修改邮箱安全设置-修改邮箱-邮箱验证码
系统管理-系统配置系统管理-安全配置
系统管理-用户管理-列表系统管理-用户管理-新增
系统管理-角色管理-列表系统管理-角色管理-新增
系统管理-菜单管理-列表系统管理-菜单管理-新增
系统管理-公告管理-列表系统管理-公告管理-修改
系统管理-字典管理-列表系统管理-字典项管理
系统管理-文件管理-列表-2系统管理-文件管理-查看文档
系统工具-代码生成-配置系统工具-代码生成-预览
系统监控-在线用户系统监控-系统日志-登录日志
系统监控-系统日志-操作日志系统监控-系统日志-操作日志-详情
+ + +## 核心技术栈 + +| 名称 | 版本 | 简介 | +|:----------------------------------------------------------------------------------------------------------------------------------|:-------------| :----------------------------------------------------------- | +| Vue | 3.5.4 | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | +| Arco Design | 2.56.0 | 字节跳动推出的前端 UI 框架,年轻化的色彩和组件设计。 | +| TypeScript | 5.0.4 | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | +| Vite | 5.1.5 | 下一代的前端工具链,为开发提供极速响应。 | +| [ContiNew Starter](https://github.com/wms-org/wms-starter) | 2.9.0 | ContiNew Starter 包含了一系列经过企业实践优化的依赖包(如 MyBatis-Plus、SaToken),可轻松集成到应用中,为开发人员减少手动引入依赖及配置的麻烦,为 Spring Boot Web 项目的灵活快速构建提供支持。 | +| Spring Boot | 3.2.12 | 简化 Spring 应用的初始搭建和开发过程,基于“约定优于配置”的理念,使开发人员不再需要定义样板化的配置。(Spring Boot 3.0 开始,要求 Java 17 作为最低版本) | +| Undertow | 2.3.17.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | +| Sa-Token + JWT | 1.39.0 | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。 | +| MyBatis Plus | 3.5.8 | MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率。 | +| dynamic-datasource-spring-boot-starter | 4.3.1 | 基于 Spring Boot 的快速集成多数据源的启动器。 | +| Hikari | 5.0.1 | JDBC 连接池,号称 “史上最快连接池”,SpringBoot 在 2.0 之后,采用的默认数据库连接池就是 Hikari。 | +| MySQL | 8.0.33 | 体积小、速度快、总体拥有成本低,是最流行的关系型数据库管理系统之一。 | +| mysql-connector-j | 8.3.0 | MySQL Java 驱动。 | +| P6Spy | 3.9.1 | SQL 性能分析组件。 | +| Liquibase | 4.24.0 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 | +| [JetCache](https://github.com/alibaba/jetcache/blob/master/docs/CN/Readme.md) | 2.7.7 | 一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了 Cache 接口用于手工缓存操作。 | +| Redisson | 3.41.0 | 不仅仅是一个 Redis Java 客户端,Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,为使用者提供了一系列具有分布式特性的常用工具:分布式锁、限流器等。 | +| Redis | 7.2.3 | 高性能的 key-value 数据库。 | +| [Snail Job](https://snailjob.opensnail.com/) | 1.2.0 | 灵活,可靠和快速的分布式任务重试和分布式任务调度平台。 | +| [X File Storage](https://x-file-storage.xuyanwu.cn/#/) | 2.2.1 | 一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台。 | +| SMS4J | 3.3.3 | 短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程。 | +| Just Auth | 1.16.7 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy! | +| Easy Excel | 3.3.4 | 一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。 | +| [AJ-Captcha](https://ajcaptcha.beliefteam.cn/captcha-doc/) | 1.3.0 | Java 行为验证码,包含滑动拼图、文字点选两种方式,UI支持弹出和嵌入两种方式。 | +| Easy Captcha | 1.6.2 | Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目。 | +| [Crane4j](https://createsequence.gitee.io/crane4j-doc/#/) | 2.9.0 | 一个基于注解的,用于完成一切 “根据 A 的 key 值拿到 B,再把 B 的属性映射到 A” 这类需求的字段填充框架。 | +| [CosID](https://cosid.ahoo.me/guide/getting-started.html) | 2.10.1 | 旨在提供通用、灵活、高性能的分布式 ID 生成器。 | +| [Graceful Response](https://doc.feiniaojin.com/graceful-response/home.html) | 5.0.4-boot3 | 一个Spring Boot技术栈下的优雅响应处理组件,可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程,提高开发效率,提高代码质量。 | +| Knife4j | 4.5.0 | 前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案。 | +| Hutool | 5.8.34 | 小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。 | +| Lombok | 1.18.36 | 在 Java 开发过程中用注解的方式,简化了 JavaBean 的编写,避免了冗余和样板式代码,让编写的类更加简洁。 | + +## 快速开始 + +> [!TIP] +> 更详细的流程,请查看在线文档[《快速开始》](https://wms.top/admin/intro/quick-start.html)。 + +```bash +# 1.克隆本项目 +git clone https://github.com/wms-org/wms-admin.git + +# 2.在 IDE(IntelliJ IDEA/Eclipse)中打开本项目 + +# 3.修改配置文件中的数据源配置信息、Redis 配置信息、邮件配置信息等 +# [3.也可以在 IntelliJ IDEA 中直接配置程序启动环境变量(DB_HOST、DB_PORT、DB_USER、DB_PWD、DB_NAME;REDIS_HOST、REDIS_PORT、REDIS_PWD、REDIS_DB)] + +# 4.启动程序 +# 4.1 启动成功:访问 http://localhost:8000/,页面输出:Xxx started successfully. +# 4.2 接口文档:http://localhost:8000/doc.html + +# 5.部署 +# 5.1 Docker 部署 +# 5.1.1 服务器安装好 docker 及 docker-compose(参考:https://blog.charles7c.top/categories/fragments/2022/10/31/CentOS%E5%AE%89%E8%A3%85Docker) +# 5.1.2 执行 mvn package 进行项目打包,将 target/app 目录下的所有内容放到 /docker/wms-admin 目录下 +# 5.1.3 将 docker 目录上传到服务器 / 目录下,并授权(chmod -R 777 /docker) +# 5.1.4 修改 docker-compose.yml 中的 MySQL 配置、Redis 配置、wms-admin-server 配置、Nginx 配置 +# 5.1.5 执行 docker-compose up -d 创建并后台运行所有容器 +# 5.2 其他方式部署 +``` + +## 项目结构 + +> [!TIP] +> 后端采用按功能拆分模块的开发方式,下方项目目录结构是按照模块的层次顺序进行介绍的,实际 IDE 中 `wms-admin-common` 模块会因为字母排序原因排在上方。 + +``` +wms-admin +├─ wms-webapi(API 及打包部署模块) +│ ├─ src +│ │ ├─ main +│ │ │ ├─ java/top/wms/admin +│ │ │ │ ├─ config (配置) +│ │ │ │ ├─ controller +│ │ │ │ │ ├─ auth(系统认证相关 API) +│ │ │ │ │ ├─ common(通用相关 API) +│ │ │ │ │ ├─ monitor(系统监控相关 API) +│ │ │ │ │ ├─ system(系统管理相关 API) +│ │ │ │ │ └─ tool(系统工具相关 API) +│ │ │ │ └─ ContiNewAdminApplication.java(ContiNew Admin 启动程序) +│ │ │ └─ resources +│ │ │ ├─ config(核心配置目录) +│ │ │ │ ├─ application-dev.yml(开发环境配置文件) +│ │ │ │ ├─ application-prod.yml(生产环境配置文件) +│ │ │ │ └─ application.yml(通用配置文件) +│ │ │ ├─ db/changelog(Liquibase 数据脚本配置目录) +│ │ │ │ ├─ mysql(MySQL 数据库初始 SQL 脚本目录) +│ │ │ │ ├─ postgresql(PostgreSQL 数据库初始 SQL 脚本目录) +│ │ │ │ └─ db.changelog-master.yaml(Liquibase 变更记录文件) +│ │ │ ├─ templates(模板配置目录,例如:邮件模板) +│ │ │ ├─ banner.txt(Banner 配置文件) +│ │ │ └─ logback-spring.xml(日志配置文件) +│ │ └─ test(测试相关代码目录) +│ └─ pom.xml(包含打包相关配置) +├─ wms-module-system(系统管理模块,存放系统管理相关业务功能,例如:部门管理、角色管理、用户管理等) +│ ├─ src +│ │ ├─ main +│ │ │ ├─ java/top/wms/admin +│ │ │ │ ├─ auth(系统认证相关业务) +│ │ │ │ │ ├─ model(系统认证相关模型) +│ │ │ │ │ │ ├─ query(系统认证相关查询条件) +│ │ │ │ │ │ ├─ req(系统认证相关请求对象(Request)) +│ │ │ │ │ │ └─ resp(系统认证相关响应对象(Response)) +│ │ │ │ │ └─ service(系统认证相关业务接口及实现类) +│ │ │ │ └─ system(系统管理相关业务) +│ │ │ │ ├─ config(系统管理相关配置) +│ │ │ │ ├─ enums(系统管理相关枚举) +│ │ │ │ ├─ mapper(系统管理相关 Mapper) +│ │ │ │ ├─ model(系统管理相关模型) +│ │ │ │ │ ├─ entity(系统管理相关实体对象) +│ │ │ │ │ ├─ query(系统管理相关查询条件) +│ │ │ │ │ ├─ req(系统管理相关请求对象(Request)) +│ │ │ │ │ └─ resp(系统管理相关响应对象(Response)) +│ │ │ │ ├─ service(系统管理相关业务接口及实现类) +│ │ │ │ └─ util(系统管理相关工具类) +│ │ │ └─ resources +│ │ │ └─ mapper(系统管理相关 Mapper XML 文件目录) +│ │ └─ test(测试相关代码目录) +│ └─ pom.xml +├─ wms-plugin(插件模块,存放代码生成、任务调度等扩展模块,后续会进行插件化改造) +│ ├─ wms-plugin-schedule(任务调度插件模块) +│ │ ├─ src +│ │ │ ├─ main/java/top/wms/admin/schedule +│ │ │ │ ├─ api(任务调度中心相关 API) +│ │ │ │ ├─ config(任务调度相关配置) +│ │ │ │ ├─ constant(任务调度相关常量) +│ │ │ │ ├─ enums(任务调度相关枚举) +│ │ │ │ ├─ model(任务调度相关模型) +│ │ │ │ │ ├─ query(任务调度相关查询条件) +│ │ │ │ │ ├─ req(任务调度相关请求对象(Request)) +│ │ │ │ │ └─ resp(任务调度相关响应对象(Response)) +│ │ │ │ └─ service(代码生成器相关业务接口及实现类) +│ │ │ └─ test(测试相关代码目录) +│ │ └─ pom.xml +│ ├─ wms-plugin-open(能力开放插件模块) +│ │ ├─ src +│ │ │ ├─ main/java/top/wms/admin/open +│ │ │ │ ├─ mapper(代码生成器相关 Mapper) +│ │ │ │ ├─ model(能力开放相关模型) +│ │ │ │ │ ├─ entity(能力开放相关实体对象) +│ │ │ │ │ ├─ query(能力开放相关查询条件) +│ │ │ │ │ ├─ req(能力开放相关请求对象(Request)) +│ │ │ │ │ └─ resp(能力开放相关响应对象(Response)) +│ │ │ │ └─ service(能力开放相关业务接口及实现类) +│ │ │ └─ test(测试相关代码目录) +│ │ └─ pom.xml +│ ├─ wms-plugin-generator(代码生成器插件模块) +│ │ ├─ src +│ │ │ ├─ main +│ │ │ │ ├─ java/top/wms/admin/generator +│ │ │ │ │ ├─ config(代码生成器相关配置) +│ │ │ │ │ ├─ enums(代码生成器相关枚举) +│ │ │ │ │ ├─ mapper(代码生成器相关 Mapper) +│ │ │ │ │ ├─ model(代码生成器相关模型) +│ │ │ │ │ │ ├─ entity(代码生成器相关实体对象) +│ │ │ │ │ │ ├─ query(代码生成器相关查询条件) +│ │ │ │ │ │ ├─ req(代码生成器相关请求对象(Request)) +│ │ │ │ │ │ └─ resp(代码生成器相关响应对象(Response)) +│ │ │ │ │ └─ service(代码生成器相关业务接口及实现类) +│ │ │ │ └─ resources +│ │ │ │ ├─ templates/generator(代码生成相关模板目录) +│ │ │ │ ├─ application.yml(代码生成配置文件) +│ │ │ │ └─ generator.properties(代码生成类型映射配置文件) +│ │ │ └─ test(测试相关代码目录) +│ │ └─ pom.xml +│ └─ pom.xml +├─ wms-common(公共模块,存放公共工具类,公共配置等) +│ ├─ src +│ │ ├─ main/java/top/wms/admin/common +│ │ │ ├─ config(公共配置) +│ │ │ ├─ constant(公共常量) +│ │ │ ├─ enums(公共枚举) +│ │ │ ├─ model(公共模型) +│ │ │ │ ├─ dto(公共 DTO(Data Transfer Object)) +│ │ │ │ ├─ req(公共请求对象(Request)) +│ │ │ │ └─ resp(公共响应对象(Response)) +│ │ │ └─ util(公共工具类) +│ │ └─ test(测试相关代码目录) +│ └─ pom.xml +├─ wms-extension(扩展模块) +│ ├─ wms-extension-schedule-server(任务调度服务端模块,实际开发时如果是公司统一提供环境,可直接删除本模块) +│ │ ├─ src +│ │ │ ├─ main +│ │ │ │ ├─ java/top/wms/admin/extension/schedule +│ │ │ │ │ └─ ScheduleServerApplication.java(任务调度服务端启动程序) +│ │ │ │ └─ resources +│ │ │ │ ├─ config(核心配置目录) +│ │ │ │ │ ├─ application-dev.yml(开发环境配置文件) +│ │ │ │ │ ├─ application-prod.yml(生产环境配置文件) +│ │ │ │ │ └─ application.yml(通用配置文件) +│ │ │ │ ├─ db/changelog(Liquibase 数据脚本配置目录) +│ │ │ │ │ ├─ mysql(MySQL 数据库初始 SQL 脚本目录) +│ │ │ │ │ ├─ postgresql(PostgreSQL 数据库初始 SQL 脚本目录) +│ │ │ │ │ └─ db.changelog-master.yaml(Liquibase 变更记录文件) +│ │ │ │ └─ logback-spring.xml(日志配置文件) +│ │ │ └─ test(测试相关代码目录) +│ │ └─ pom.xml +│ └─ pom.xml +├─ .github(GitHub 相关配置目录,实际开发时直接删除) +├─ .idea +│ └─ icon.png(IDEA 项目图标,实际开发时直接删除) +├─ .image(截图目录,实际开发时直接删除) +├─ .style(代码格式、License文件头相关配置目录,实际开发时根据需要取舍,删除时注意删除 spotless 插件配置) +├─ .gitignore(Git 忽略文件相关配置文件) +├─ docker(项目部署相关配置目录,实际开发时可备份后直接删除) +├─ LICENSE(开源协议文件) +├─ CHANGELOG.md(更新日志文件,实际开发时直接删除) +├─ README.md(项目 README 文件,实际开发时替换为真实内容) +├─ lombok.config(Lombok 全局配置文件) +└─ pom.xml(包含版本锁定及全局插件相关配置) +``` + +## 贡献指南 + +ContiNew Admin 致力于提供开箱即用,持续舒适的开发体验。作为一个开源项目,Creator 的初心是希望 ContiNew Admin 依托开源协作模式,提升技术透明度、放大集体智慧、共创优秀实践,源源不断地为企业级项目开发提供助力。 + +我们非常欢迎广大社区用户为 ContiNew Admin **贡献(开发,测试、文档、答疑等)** 或优化代码,欢迎各位感兴趣的小伙伴儿,[添加微信](https://wms.top/support.html) 讨论或认领任务。 + +### 分支说明 + +ContiNew Admin 的分支目前分为下个大版本的开发分支和上个大版本的维护分支,PR 前请注意对应分支是否处于维护状态,版本支持情况请查看 [更新日志/版本支持](https://wms.top/admin/other/changelog.html#%E7%89%88%E6%9C%AC%E6%94%AF%E6%8C%81)。 + +| 分支 | 说明 | +| ----- | ------------------------------------------------------------ | +| dev | 开发分支,默认为下个大版本的 SNAPSHOT 版本,接受新功能或新功能优化 PR | +| x.x.x | 维护分支,在 vx.x.x 版本维护期终止前(一般为下个大版本发布前),用于修复上个版本的 Bug,只接受已有功能修复,不接受新功能 PR | + +### 贡献代码 + +如果您想提交新功能或优化现有代码,可以按照以下步骤操作: + +1. 首先,在 Gitee 或 Github 上将项目 fork 到您自己的仓库 +2. 然后,将 fork 过来的项目(即您的项目)克隆到本地 +3. 切换到当前仍在维护的分支(请务必充分了解分支使用说明,可进群联系维护者确认) +4. 开始修改代码,修改完成后,将代码 commit 并 push 到您的远程仓库 +5. 在 Gitee 或 Github 上新建 pull request(pr),选择好源和目标,按模板要求填写说明信息后提交即可(多多参考 [已批准合并的 pr 记录](https://github.com/wms-org/wms-admin/pulls?q=is%3Apr+is%3Amerged),会大大增加批准合并率) +6. 最后,耐心等待维护者合并您的请求即可 + +请记住,如果您有任何疑问或需要帮助,我们将随时提供支持。 + +> [!IMPORTANT] +> 欢迎大家为 ContiNew Admin 贡献代码,我们非常感谢您的支持!为了更好地管理项目,维护者有一些要求: +> +> 1. 请确保代码、配置文件的结构和命名规范良好,完善的代码注释甚至包括接口文档参数示例,并遵循阿里巴巴的 《Java开发手册(黄山版)》 中的代码规范,保证代码质量和可维护性 +> 2. 在提交代码前,请按照 [Angular 提交规范](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 编写 commit 的 message(建议在 IntelliJ IDEA 中下载并安装 Git Commit Template 插件,以便按照规范进行 commit) +> 3. 提交代码之前,请关闭所有代码窗口,执行 `mvn compile` 命令(代码格式化插件会在项目编译时对全局代码进行格式修正),编译通过后,不要再打开查看任何代码窗口,直接提交即可,以免不同的 IDE 配置会自动进行代码格式化 + +## 反馈交流 + +欢迎各位小伙伴儿扫描下方二维码加入项目交流群,与项目维护团队及其他大佬用户实时交流讨论。 + +
+ 二维码 +
+ +## 鸣谢 + +### 鸣谢 + +感谢参与贡献的每一位小伙伴🥰 + + + contributors + + +### 特别鸣谢 + +- 感谢 JetBrains 提供的 非商业开源软件开发授权 +- 感谢 MyBatis PlusSa-TokenJetCacheCrane4jKnife4jHutool 等开源组件作者为国内开源世界作出的贡献 +- 感谢项目使用或未使用到的每一款开源组件,致敬各位开源先驱 :fire: + +## License + +- 遵循 Apache-2.0 开源许可协议 +- Copyright © 2022-present Charles7c + +## GitHub Star 趋势 + +![GitHub Star 趋势](https://starchart.cc/charles7c/wms-admin.svg) \ No newline at end of file diff --git a/dat/AfterConvert_RGB.raw b/dat/AfterConvert_RGB.raw new file mode 100644 index 0000000..d513dfa Binary files /dev/null and b/dat/AfterConvert_RGB.raw differ diff --git a/docker/continew-admin/Dockerfile b/docker/continew-admin/Dockerfile new file mode 100644 index 0000000..2647684 --- /dev/null +++ b/docker/continew-admin/Dockerfile @@ -0,0 +1,14 @@ +FROM openjdk:17 + +MAINTAINER Charles7c charles7c@126.com + +ARG JAR_FILE=./bin/*.jar +COPY ${JAR_FILE} /app/bin/app.jar +WORKDIR /app/bin + +ENTRYPOINT ["java", \ + "-jar", \ + "-XX:+UseZGC", \ + "-Djava.security.egd=file:/dev/./urandom", \ + "-Dspring.profiles.active=prod", \ + "app.jar"] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..a0bba57 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,112 @@ +version: '3' +services: + mysql: + image: mysql:8.0.33 + restart: always + container_name: mysql + ports: + - '3306:3306' + environment: + TZ: Asia/Shanghai + MYSQL_ROOT_PASSWORD: 你的root用户密码 + # 初始化数据库(后续的初始化 SQL 会在这个库执行) + MYSQL_DATABASE: wms_admin + #MYSQL_USER: 你的数据库用户名 + #MYSQL_PASSWORD: 你的数据库密码 + volumes: + - /docker/mysql/conf/:/etc/mysql/conf.d/ + - /docker/mysql/data/:/var/lib/mysql/ + command: + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + --collation-server=utf8mb4_general_ci + --explicit_defaults_for_timestamp=true + --lower_case_table_names=1 +# postgresql: +# image: postgres:14.2 +# restart: always +# container_name: postgresql +# ports: +# - '5432:5432' +# environment: +# TZ: Asia/Shanghai +# POSTGRES_USER: 你的用户名 +# POSTGRES_PASSWORD: 你的用户密码 +# POSTGRES_DB: wms_admin +# volumes: +# - /docker/postgresql/data/:/var/lib/postgresql/data/ + redis: + image: redis:7.2.3 + restart: always + container_name: redis + ports: + - '6379:6379' + environment: + TZ: Asia/Shanghai + volumes: + - /docker/redis/conf/redis.conf:/usr/local/redis/config/redis.conf + - /docker/redis/data/:/data/ + - /docker/redis/logs/:/logs/ + command: 'redis-server /usr/local/redis/config/redis.conf --appendonly yes --requirepass 你的 Redis 密码' + wms-admin-server: + build: ./wms-admin + restart: always + container_name: wms-admin-server + ports: + - '18000:18000' + - '1789:1789' + environment: + TZ: Asia/Shanghai + DB_HOST: 172.17.0.1 + DB_PORT: 3306 + DB_USER: 你的数据库用户名 + DB_PWD: 你的数据库密码 + DB_NAME: wms_admin + REDIS_HOST: 172.17.0.1 + REDIS_PORT: 6379 + REDIS_PWD: 你的 Redis 密码 + REDIS_DB: 0 + SCHEDULE_HOST: 172.17.0.1 + SCHEDULE_PORT: 1788 + SCHEDULE_TOKEN: 任务调度服务端 Token + volumes: + - /docker/wms-admin/config/:/app/config/ + - /docker/wms-admin/data/file/:/app/data/file/ + - /docker/wms-admin/logs/:/app/logs/ + - /docker/wms-admin/lib/:/app/lib/ + depends_on: + - redis + - mysql + schedule-server: + build: ./schedule-server + restart: always + container_name: wms-admin-schedule-server + ports: + - '18001:18001' + - '1788:1788' + environment: + TZ: Asia/Shanghai + DB_HOST: 172.17.0.1 + DB_PORT: 3306 + DB_USER: 你的数据库用户名 + DB_PWD: 你的数据库密码 + DB_NAME: wms_admin_job + volumes: + - /docker/schedule-server/logs/:/app/logs/ + depends_on: + - mysql + nginx: + image: nginx:1.25.3 + restart: always + container_name: nginx + ports: + - '80:80' + - '443:443' + environment: + TZ: Asia/Shanghai + volumes: + - /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf + - /docker/nginx/cert/:/etc/nginx/cert/ + - /docker/nginx/logs/:/var/log/nginx/ + # 前端页面目录 + - /docker/wms-admin/html/:/usr/share/nginx/html/ diff --git a/docker/nginx/conf/nginx.conf b/docker/nginx/conf/nginx.conf new file mode 100644 index 0000000..4b573d0 --- /dev/null +++ b/docker/nginx/conf/nginx.conf @@ -0,0 +1,112 @@ +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + # 限制 body 大小 + client_max_body_size 100m; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # 后端项目 + upstream admin-server { + ip_hash; + server 172.17.0.1:18000; + } + + server { + listen 443 ssl; + server_name api.wms.top; + + # 证书直接存放 /docker/nginx/cert 目录下即可(更改证书名称即可,无需更改证书路径) + ssl_certificate /etc/nginx/cert/xxx.local.pem; # /etc/nginx/cert/ 为 docker 映射路径 不允许更改 + ssl_certificate_key /etc/nginx/cert/xxx.local.key; # /etc/nginx/cert/ 为 docker 映射路径 不允许更改 + ssl_session_timeout 5m; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + + location / { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_ignore_client_abort on; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://admin-server/; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } + + # HTTP 请求 将转发到 HTTPS + server { + listen 80; + server_name api.wms.top; + rewrite ^ https://$http_host$request_uri? permanent; + } + + # 前端项目 + server { + listen 443 ssl; + server_name admin.wms.top; + + # 证书直接存放 /docker/nginx/cert 目录下即可(更改证书名称即可,无需更改证书路径) + ssl_certificate /etc/nginx/cert/xxx.local.pem; # /etc/nginx/cert/ 为 docker 映射路径 不允许更改 + ssl_certificate_key /etc/nginx/cert/xxx.local.key; # /etc/nginx/cert/ 为 docker 映射路径 不允许更改 + ssl_session_timeout 5m; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + index index.html index.htm; + # Nginx 部署时,POST 请求本地静态文件会返回 405 错误(Not Allowed) + # 用于解决一半 mock 数据,一半后端接口的情况 + error_page 405 =200 https://$host$request_uri; + } + + # /api/ 代理到后端(如果使用 /api/ 前缀代理而不使用 api 域名提供后端服务,可放开此配置) + #location /api/ { + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + # proxy_ignore_client_abort on; + # proxy_set_header Host $http_host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_pass http://admin-server/; + #} + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } + + # 将 HTTP 请求转发到 HTTPS + server { + listen 80; + server_name admin.wms.top; + rewrite ^ https://$http_host$request_uri? permanent; + } +} diff --git a/docker/redis/conf/redis.conf b/docker/redis/conf/redis.conf new file mode 100644 index 0000000..ad9246d --- /dev/null +++ b/docker/redis/conf/redis.conf @@ -0,0 +1,29 @@ +bind 0.0.0.0 +# redis 密码 +requirepass 你的 Redis 密码 + +# key 监听器配置 +# notify-keyspace-events Ex + +# 配置持久化文件存储路径 +dir ../data +# 配置rdb +# 15分钟内有至少1个key被更改则进行快照 +save 900 1 +# 5分钟内有至少10个key被更改则进行快照 +save 300 10 +# 1分钟内有至少10000个key被更改则进行快照 +save 60 10000 +# 开启压缩 +rdbcompression yes +# rdb文件名 用默认的即可 +dbfilename dump.rdb + +# 开启aof +appendonly yes +# 文件名 +appendfilename "appendonly.aof" +# 持久化策略,no:不同步,everysec:每秒一次,always:总是同步,速度比较慢 +# appendfsync always +appendfsync everysec +# appendfsync no diff --git a/docker/redis/data/README.md b/docker/redis/data/README.md new file mode 100644 index 0000000..a7ce096 --- /dev/null +++ b/docker/redis/data/README.md @@ -0,0 +1 @@ +Redis 数据存储目录,请确保赋予了读写权限,否则将无法写入数据 \ No newline at end of file diff --git a/docker/schedule-server/Dockerfile b/docker/schedule-server/Dockerfile new file mode 100644 index 0000000..2db96c1 --- /dev/null +++ b/docker/schedule-server/Dockerfile @@ -0,0 +1,14 @@ +FROM openjdk:17 + +MAINTAINER Charles7c charles7c@126.com + +ARG JAR_FILE=*.jar +COPY ${JAR_FILE} /app/bin/app.jar +WORKDIR /app/bin + +ENTRYPOINT ["java", \ + "-jar", \ + "-XX:+UseZGC", \ + "-Djava.security.egd=file:/dev/./urandom", \ + "-Dspring.profiles.active=prod", \ + "app.jar"] \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..1135ca1 --- /dev/null +++ b/lombok.config @@ -0,0 +1,7 @@ +config.stopBubbling=true +lombok.toString.callSuper=CALL +lombok.equalsAndHashCode.callSuper=CALL +clear lombok.val.flagUsage +lombok.val.flagUsage=ERROR +clear lombok.accessors.flagUsage +lombok.accessors.flagUsage=ERROR \ No newline at end of file diff --git a/mes-common/pom.xml b/mes-common/pom.xml new file mode 100644 index 0000000..ac576d2 --- /dev/null +++ b/mes-common/pom.xml @@ -0,0 +1,166 @@ + + + 4.0.0 + + top.wms + wms-admin + ${revision} + + + wms-common + 公共模块(存放公共工具类,公共配置等) + + + + + me.ahoo.cosid + cosid-spring-boot-starter + + + me.ahoo.cosid + cosid-spring-redis + + + + + org.dromara.sms4j + sms4j-spring-boot-starter + + + + + org.dromara.x-file-storage + x-file-storage-spring + 2.2.1 + + + + com.amazonaws + aws-java-sdk-s3 + 1.12.780 + + + + + org.freemarker + freemarker + + + + + com.mysql + mysql-connector-j + + + + + top.continew + continew-starter-extension-crud-mp + + + + + top.continew + continew-starter-auth-satoken + + + org.springframework.boot + spring-boot-starter-web + + + + + + + top.continew + continew-starter-auth-justauth + + + + + top.continew + continew-starter-cache-jetcache + + + + + top.continew + continew-starter-extension-datapermission-mp + + + + + top.continew + continew-starter-messaging-websocket + + + org.springframework.boot + spring-boot-starter-web + + + + + + + top.continew + continew-starter-messaging-mail + + + + + top.continew + continew-starter-captcha-graphic + + + + + top.continew + continew-starter-captcha-behavior + + + + + top.continew + continew-starter-security-limiter + + + + + top.continew + continew-starter-security-crypto + + + + + top.continew + continew-starter-security-mask + + + + + top.continew + continew-starter-security-password + + + + + top.continew + continew-starter-json-jackson + + + + org.springframework + spring-test + + + + io.netty + netty-all + 4.1.100.Final + + + + + diff --git a/mes-common/src/main/java/top/mes/admin/common/config/doc/GlobalAuthenticationCustomizer.java b/mes-common/src/main/java/top/mes/admin/common/config/doc/GlobalAuthenticationCustomizer.java new file mode 100644 index 0000000..80987ca --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/doc/GlobalAuthenticationCustomizer.java @@ -0,0 +1,200 @@ +package top.wms.admin.common.config.doc; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.map.MapUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.*; +import top.continew.starter.apidoc.autoconfigure.SpringDocExtensionProperties; +import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 全局鉴权参数定制器 + * + * @author echo + * @since 2024/12/31 13:36 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalAuthenticationCustomizer implements GlobalOpenApiCustomizer { + + private final SpringDocExtensionProperties properties; + private final SaTokenExtensionProperties saTokenExtensionProperties; + private final ApplicationContext context; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + /** + * 定制 OpenAPI 文档 + * + * @param openApi 当前 OpenAPI 对象 + */ + @Override + public void customise(OpenAPI openApi) { + if (MapUtil.isEmpty(openApi.getPaths())) { + return; + } + + // 收集需要排除的路径(包括 Sa-Token 配置中的排除路径和 @SaIgnore 注解路径) + Set excludedPaths = collectExcludedPaths(); + + // 遍历所有路径,为需要鉴权的路径添加安全认证配置 + openApi.getPaths().forEach((path, pathItem) -> { + if (isPathExcluded(path, excludedPaths)) { + // 路径在排除列表中,跳过处理 + return; + } + // 为路径添加安全认证参数 + addAuthenticationParameters(pathItem); + }); + } + + /** + * 收集所有需要排除的路径 + * + * @return 排除路径集合 + */ + private Set collectExcludedPaths() { + Set excludedPaths = new HashSet<>(); + excludedPaths.addAll(Arrays.asList(saTokenExtensionProperties.getSecurity().getExcludes())); + excludedPaths.addAll(resolveSaIgnorePaths()); + return excludedPaths; + } + + /** + * 为路径项添加认证参数 + * + * @param pathItem 当前路径项 + */ + private void addAuthenticationParameters(PathItem pathItem) { + Components components = properties.getComponents(); + if (components == null || MapUtil.isEmpty(components.getSecuritySchemes())) { + return; + } + Map securitySchemes = components.getSecuritySchemes(); + List schemeNames = securitySchemes.values().stream().map(SecurityScheme::getName).toList(); + pathItem.readOperations().forEach(operation -> { + SecurityRequirement securityRequirement = new SecurityRequirement(); + schemeNames.forEach(securityRequirement::addList); + operation.addSecurityItem(securityRequirement); + }); + } + + /** + * 解析所有带有 @SaIgnore 注解的路径 + * + * @return 被忽略的路径集合 + */ + private Set resolveSaIgnorePaths() { + // 获取所有标注 @RestController 的 Bean + Map controllers = context.getBeansWithAnnotation(RestController.class); + Set ignoredPaths = new HashSet<>(); + + // 遍历所有控制器,解析 @SaIgnore 注解路径 + controllers.values().forEach(controllerBean -> { + Class controllerClass = AopUtils.getTargetClass(controllerBean); + List classPaths = getClassPaths(controllerClass); + + // 类级别的 @SaIgnore 注解 + if (controllerClass.isAnnotationPresent(SaIgnore.class)) { + classPaths.forEach(classPath -> ignoredPaths.add(classPath + "/**")); + } + + // 方法级别的 @SaIgnore 注解 + Arrays.stream(controllerClass.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(SaIgnore.class)) + .forEach(method -> ignoredPaths.addAll(combinePaths(classPaths, getMethodPaths(method)))); + }); + + return ignoredPaths; + } + + /** + * 获取类上的所有路径 + * + * @param controller 控制器类 + * @return 类路径列表 + */ + private List getClassPaths(Class controller) { + List classPaths = new ArrayList<>(); + // 处理 @RequestMapping 注解 + if (controller.isAnnotationPresent(RequestMapping.class)) { + RequestMapping mapping = controller.getAnnotation(RequestMapping.class); + classPaths.addAll(Arrays.asList(mapping.value())); + } + // 处理 @CrudRequestMapping 注解 + if (controller.isAnnotationPresent(CrudRequestMapping.class)) { + CrudRequestMapping mapping = controller.getAnnotation(CrudRequestMapping.class); + if (!mapping.value().isEmpty()) { + classPaths.add(mapping.value()); + } + } + return classPaths; + } + + /** + * 获取方法上的所有路径 + * + * @param method 控制器方法 + * @return 方法路径列表 + */ + private List getMethodPaths(Method method) { + List methodPaths = new ArrayList<>(); + + // 检查方法上的各种映射注解 + if (method.isAnnotationPresent(GetMapping.class)) { + methodPaths.addAll(Arrays.asList(method.getAnnotation(GetMapping.class).value())); + } else if (method.isAnnotationPresent(PostMapping.class)) { + methodPaths.addAll(Arrays.asList(method.getAnnotation(PostMapping.class).value())); + } else if (method.isAnnotationPresent(PutMapping.class)) { + methodPaths.addAll(Arrays.asList(method.getAnnotation(PutMapping.class).value())); + } else if (method.isAnnotationPresent(DeleteMapping.class)) { + methodPaths.addAll(Arrays.asList(method.getAnnotation(DeleteMapping.class).value())); + } else if (method.isAnnotationPresent(RequestMapping.class)) { + methodPaths.addAll(Arrays.asList(method.getAnnotation(RequestMapping.class).value())); + } else if (method.isAnnotationPresent(PatchMapping.class)) { + methodPaths.addAll(Arrays.asList(method.getAnnotation(PatchMapping.class).value())); + } + + return methodPaths; + } + + /** + * 组合类路径和方法路径 + * + * @param classPaths 类路径列表 + * @param methodPaths 方法路径列表 + * @return 完整路径集合 + */ + private Set combinePaths(List classPaths, List methodPaths) { + return classPaths.stream() + .flatMap(classPath -> methodPaths.stream().map(methodPath -> classPath + methodPath)) + .collect(Collectors.toSet()); + } + + /** + * 检查路径是否在排除列表中 + * + * @param path 当前路径 + * @param excludedPaths 排除路径集合,支持通配符 + * @return 是否匹配排除规则 + */ + private boolean isPathExcluded(String path, Set excludedPaths) { + return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path)); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/doc/GlobalDescriptionCustomizer.java b/mes-common/src/main/java/top/mes/admin/common/config/doc/GlobalDescriptionCustomizer.java new file mode 100644 index 0000000..d9f977f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/doc/GlobalDescriptionCustomizer.java @@ -0,0 +1,49 @@ +package top.wms.admin.common.config.doc; + +import cn.hutool.core.util.StrUtil; +import io.swagger.v3.oas.models.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import java.util.ArrayList; +import java.util.List; + +/** + * 全局描述定制器 - 处理 sa-token 的注解权限码 + * + * @author echo + * @since 2025/1/24 14:59 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalDescriptionCustomizer implements GlobalOperationCustomizer { + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + // 将 sa-token 注解数据添加到 operation 的描述中 + // 权限 + List noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod)); + + // 如果注解数据列表为空,直接返回原 operation + if (noteList.isEmpty()) { + return operation; + } + // 拼接注解数据为字符串 + String noteStr = StrUtil.join("
", noteList); + // 获取原描述 + String originalDescription = operation.getDescription(); + // 根据原描述是否为空,更新描述 + String newDescription = StringUtils.isNotEmpty(originalDescription) + ? originalDescription + "
" + noteStr + : noteStr; + + // 设置新描述 + operation.setDescription(newDescription); + return operation; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/doc/OperationDescriptionCustomizer.java b/mes-common/src/main/java/top/mes/admin/common/config/doc/OperationDescriptionCustomizer.java new file mode 100644 index 0000000..256fabb --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/doc/OperationDescriptionCustomizer.java @@ -0,0 +1,164 @@ +package top.wms.admin.common.config.doc; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaMode; +import cn.hutool.core.text.CharSequenceUtil; +import org.springframework.web.method.HandlerMethod; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.extension.crud.annotation.CrudApi; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +/** + * Operation 描述定制器 处理 sa-token 鉴权标识符 + * + * @author echo + * @since 2024/6/14 11:18 + */ +public class OperationDescriptionCustomizer { + + /** + * 获取 sa-token 注解信息 + * + * @param handlerMethod 处理程序方法 + * @return 包含权限和角色校验信息的列表 + */ + public List getPermission(HandlerMethod handlerMethod) { + List values = new ArrayList<>(); + + // 获取权限校验信息 + String permissionInfo = getAnnotationInfo(handlerMethod, SaCheckPermission.class, "权限校验:"); + if (!permissionInfo.isEmpty()) { + values.add(permissionInfo); + } + + // 获取角色校验信息 + String roleInfo = getAnnotationInfo(handlerMethod, SaCheckRole.class, "角色校验:"); + if (!roleInfo.isEmpty()) { + values.add(roleInfo); + } + + // 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息 + String crudPermissionInfo = getCrudPermissionInfo(handlerMethod); + if (!crudPermissionInfo.isEmpty()) { + values.add(crudPermissionInfo); + } + return values; + } + + /** + * 获取类和方法上指定注解的信息 + * + * @param handlerMethod 处理程序方法 + * @param annotationClass 注解类 + * @param title 信息标题 + * @param 注解类型 + * @return 拼接好的注解信息字符串 + */ + @SuppressWarnings("unchecked") + private String getAnnotationInfo(HandlerMethod handlerMethod, + Class annotationClass, + String title) { + StringBuilder infoBuilder = new StringBuilder(); + + // 获取类上的注解 + A classAnnotation = handlerMethod.getBeanType().getAnnotation(annotationClass); + if (classAnnotation != null) { + appendAnnotationInfo(infoBuilder, "类:", classAnnotation); + } + + // 获取方法上的注解 + A methodAnnotation = handlerMethod.getMethodAnnotation(annotationClass); + if (methodAnnotation != null) { + appendAnnotationInfo(infoBuilder, "方法:", methodAnnotation); + } + + // 如果有注解信息,添加标题 + if (!infoBuilder.isEmpty()) { + infoBuilder.insert(0, "" + title + "
"); + } + + return infoBuilder.toString(); + } + + /** + * 拼接注解信息到 StringBuilder 中 + * + * @param builder 用于拼接信息的 StringBuilder + * @param prefix 前缀信息,如 "类:" 或 "方法:" + * @param annotation 注解对象 + */ + private void appendAnnotationInfo(StringBuilder builder, String prefix, Annotation annotation) { + String[] values = null; + SaMode mode = null; + String type = ""; + String[] orRole = new String[0]; + + if (annotation instanceof SaCheckPermission checkPermission) { + values = checkPermission.value(); + mode = checkPermission.mode(); + type = checkPermission.type(); + orRole = checkPermission.orRole(); + } else if (annotation instanceof SaCheckRole checkRole) { + values = checkRole.value(); + mode = checkRole.mode(); + type = checkRole.type(); + } + + if (values != null && mode != null) { + builder.append(""); + builder.append(prefix); + if (!type.isEmpty()) { + builder.append("(类型:").append(type).append(")"); + } + builder.append(getAnnotationNote(values, mode)); + if (orRole.length > 0) { + builder.append(" 或 角色校验(").append(getAnnotationNote(orRole, mode)).append(")"); + } + builder.append("
"); + } + } + + /** + * 根据注解的模式拼接注解值 + * + * @param values 注解的值数组 + * @param mode 注解的模式(AND 或 OR) + * @return 拼接好的注解值字符串 + */ + private String getAnnotationNote(String[] values, SaMode mode) { + if (mode.equals(SaMode.AND)) { + return String.join(" 且 ", values); + } else { + return String.join(" 或 ", values); + } + } + + /** + * 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息 + * + * @param handlerMethod 处理程序方法 + * @return 拼接好的权限信息字符串 + */ + private String getCrudPermissionInfo(HandlerMethod handlerMethod) { + CrudRequestMapping crudRequestMapping = handlerMethod.getBeanType().getAnnotation(CrudRequestMapping.class); + CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class); + + if (crudRequestMapping == null || crudApi == null) { + return ""; + } + + String path = crudRequestMapping.value(); + String prefix = String.join(StringConstants.COLON, CharSequenceUtil.splitTrim(path, StringConstants.SLASH)); + Api api = crudApi.value(); + String apiName = Api.PAGE.equals(api) || Api.TREE.equals(api) ? Api.LIST.name() : api.name(); + String permission = "%s:%s".formatted(prefix, apiName.toLowerCase()); + + return "Crud 权限校验:
方法:" + permission + ""; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/exception/GlobalExceptionHandler.java b/mes-common/src/main/java/top/mes/admin/common/config/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..93911df --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/exception/GlobalExceptionHandler.java @@ -0,0 +1,105 @@ +package top.wms.admin.common.config.exception; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.text.CharSequenceUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.servlet.NoHandlerFoundException; +import top.continew.starter.core.exception.BadRequestException; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.web.model.R; + +/** + * 全局异常处理器 + * + * @author Charles7c + * @author echo + * @since 2024/8/7 20:21 + */ +@Slf4j +@Order(99) +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 拦截业务异常 + */ + @ExceptionHandler(BusinessException.class) + public R handleBusinessException(BusinessException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage()); + } + + /** + * 拦截自定义验证异常-错误请求 + */ + @ExceptionHandler(BadRequestException.class) + public R handleBadRequestException(BadRequestException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), e.getMessage()); + } + + /** + * 拦截校验异常-方法参数类型不匹配异常 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, + HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "参数 '%s' 类型不匹配".formatted(e.getName())); + } + + /** + * 拦截文件上传异常-超过上传大小限制 + */ + @ExceptionHandler(MultipartException.class) + public R handleMultipartException(MultipartException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + String msg = e.getMessage(); + R defaultFail = R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), msg); + if (CharSequenceUtil.isBlank(msg)) { + return defaultFail; + } + String sizeLimit; + Throwable cause = e.getCause(); + if (null != cause) { + msg = msg.concat(cause.getMessage().toLowerCase()); + } + if (msg.contains("larger than")) { + sizeLimit = CharSequenceUtil.subAfter(msg, "larger than ", true); + } else if (msg.contains("size") && msg.contains("exceed")) { + sizeLimit = CharSequenceUtil.subBetween(msg, "the maximum size ", " for"); + } else { + return defaultFail; + } + return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "请上传小于 %s 的文件".formatted(FileUtil + .readableFileSize(Long.parseLong(sizeLimit)))); + } + + /** + * 拦截请求 URL 不存在异常 + */ + @ExceptionHandler(NoHandlerFoundException.class) + public R handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.NOT_FOUND.value()), "请求 URL '%s' 不存在".formatted(request + .getRequestURI())); + } + + /** + * 拦截不支持的 HTTP 请求方法异常 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), "请求方式 '%s' 不支持".formatted(e.getMethod())); + } +} \ No newline at end of file diff --git a/mes-common/src/main/java/top/mes/admin/common/config/exception/GlobalSaTokenExceptionHandler.java b/mes-common/src/main/java/top/mes/admin/common/config/exception/GlobalSaTokenExceptionHandler.java new file mode 100644 index 0000000..2841060 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/exception/GlobalSaTokenExceptionHandler.java @@ -0,0 +1,56 @@ +package top.wms.admin.common.config.exception; + +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import top.continew.starter.web.model.R; + +/** + * 全局 SaToken 异常处理器 + * + * @author Charles7c + * @since 2024/8/7 20:21 + */ +@Slf4j +@Order(99) +@RestControllerAdvice +public class GlobalSaTokenExceptionHandler { + + /** + * 认证异常-登录认证 + */ + @ExceptionHandler(NotLoginException.class) + public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + String errorMsg = switch (e.getType()) { + case NotLoginException.KICK_OUT -> "您已被踢下线"; + case NotLoginException.BE_REPLACED_MESSAGE -> "您已被顶下线"; + default -> "您的登录状态已过期,请重新登录"; + }; + return R.fail(String.valueOf(HttpStatus.UNAUTHORIZED.value()), errorMsg); + } + + /** + * 认证异常-权限认证 + */ + @ExceptionHandler(NotPermissionException.class) + public R handleNotPermissionException(NotPermissionException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.FORBIDDEN.value()), "没有访问权限,请联系管理员授权"); + } + + /** + * 认证异常-角色认证 + */ + @ExceptionHandler(NotRoleException.class) + public R handleNotRoleException(NotRoleException e, HttpServletRequest request) { + log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); + return R.fail(String.valueOf(HttpStatus.FORBIDDEN.value()), "没有访问权限,请联系管理员授权"); + } +} \ No newline at end of file diff --git a/mes-common/src/main/java/top/mes/admin/common/config/mybatis/BCryptEncryptor.java b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/BCryptEncryptor.java new file mode 100644 index 0000000..3daa847 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/BCryptEncryptor.java @@ -0,0 +1,29 @@ +package top.wms.admin.common.config.mybatis; + +import org.springframework.security.crypto.password.PasswordEncoder; +import top.continew.starter.security.crypto.encryptor.IEncryptor; + +/** + * BCrypt 加/解密处理器(不可逆) + * + * @author Charles7c + * @since 2024/2/8 22:29 + */ +public class BCryptEncryptor implements IEncryptor { + + private final PasswordEncoder passwordEncoder; + + public BCryptEncryptor(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } + + @Override + public String encrypt(String plaintext, String password, String publicKey) throws Exception { + return passwordEncoder.encode(plaintext); + } + + @Override + public String decrypt(String ciphertext, String password, String privateKey) throws Exception { + return ciphertext; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/mybatis/DataPermissionMapper.java b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/DataPermissionMapper.java new file mode 100644 index 0000000..2f208b1 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/DataPermissionMapper.java @@ -0,0 +1,41 @@ +package top.wms.admin.common.config.mybatis; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import org.apache.ibatis.annotations.Param; +import top.continew.starter.data.mp.base.BaseMapper; +import top.continew.starter.extension.datapermission.annotation.DataPermission; + +import java.util.List; + +/** + * 数据权限 Mapper 基类 + * + * @param 实体类 + * @author Charles7c + * @since 2023/9/3 21:50 + */ +public interface DataPermissionMapper extends BaseMapper { + + /** + * 根据 entity 条件,查询全部记录 + * + * @param queryWrapper 实体对象封装操作类(可以为 null) + * @return 全部记录 + */ + @Override + @DataPermission + List selectList(@Param(Constants.WRAPPER) Wrapper queryWrapper); + + /** + * 根据 entity 条件,查询全部记录(并翻页) + * + * @param page 分页查询条件 + * @param queryWrapper 实体对象封装操作类(可以为 null) + * @return 全部记录(并翻页) + */ + @Override + @DataPermission + List selectList(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/mybatis/DefaultDataPermissionUserContextProvider.java b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/DefaultDataPermissionUserContextProvider.java new file mode 100644 index 0000000..02d104d --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/DefaultDataPermissionUserContextProvider.java @@ -0,0 +1,37 @@ +package top.wms.admin.common.config.mybatis; + +import cn.hutool.core.convert.Convert; +import top.wms.admin.common.context.UserContextHolder; +import top.continew.starter.extension.datapermission.enums.DataScope; +import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider; +import top.continew.starter.extension.datapermission.model.RoleContext; +import top.continew.starter.extension.datapermission.model.UserContext; + +import java.util.stream.Collectors; + +/** + * 数据权限用户上下文提供者 + * + * @author Charles7c + * @since 2023/12/21 21:19 + */ +public class DefaultDataPermissionUserContextProvider implements DataPermissionUserContextProvider { + + @Override + public boolean isFilter() { + return !UserContextHolder.isAdmin(); + } + + @Override + public UserContext getUserContext() { + top.wms.admin.common.context.UserContext context = UserContextHolder.getContext(); + UserContext userContext = new UserContext(); + userContext.setUserId(Convert.toStr(context.getId())); + userContext.setDeptId(Convert.toStr(context.getDeptId())); + userContext.setRoles(context.getRoles() + .stream() + .map(r -> new RoleContext(Convert.toStr(r.getId()), DataScope.valueOf(r.getDataScope().name()))) + .collect(Collectors.toSet())); + return userContext; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/mybatis/MyBatisPlusMetaObjectHandler.java b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/MyBatisPlusMetaObjectHandler.java new file mode 100644 index 0000000..e925383 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/MyBatisPlusMetaObjectHandler.java @@ -0,0 +1,96 @@ +package top.wms.admin.common.config.mybatis; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.model.entity.BaseDO; + +import java.time.LocalDateTime; + +/** + * MyBatis Plus 元对象处理器配置(插入或修改时自动填充) + * + * @author Charles7c + * @since 2022/12/22 19:52 + */ +public class MyBatisPlusMetaObjectHandler implements MetaObjectHandler { + + /** + * 创建人 + */ + private static final String CREATE_USER = "createUser"; + /** + * 创建时间 + */ + private static final String CREATE_TIME = "createTime"; + /** + * 修改人 + */ + private static final String UPDATE_USER = "updateUser"; + /** + * 修改时间 + */ + private static final String UPDATE_TIME = "updateTime"; + + /** + * 插入数据时填充 + * + * @param metaObject 元对象 + */ + @Override + public void insertFill(MetaObject metaObject) { + if (null == metaObject) { + return; + } + Long createUser = UserContextHolder.getUserId(); + LocalDateTime createTime = LocalDateTime.now(); + if (metaObject.getOriginalObject() instanceof BaseDO baseDO) { + // 继承了 BaseDO 的类,填充创建信息字段 + baseDO.setCreateUser(ObjectUtil.defaultIfNull(baseDO.getCreateUser(), createUser)); + baseDO.setCreateTime(ObjectUtil.defaultIfNull(baseDO.getCreateTime(), createTime)); + } else { + // 未继承 BaseDO 的类,如存在创建信息字段则进行填充 + this.fillFieldValue(metaObject, CREATE_USER, createUser, false); + this.fillFieldValue(metaObject, CREATE_TIME, createTime, false); + } + } + + /** + * 修改数据时填充 + * + * @param metaObject 元对象 + */ + @Override + public void updateFill(MetaObject metaObject) { + if (null == metaObject) { + return; + } + Long updateUser = UserContextHolder.getUserId(); + LocalDateTime updateTime = LocalDateTime.now(); + if (metaObject.getOriginalObject() instanceof BaseDO baseDO) { + // 继承了 BaseDO 的类,填充修改信息 + baseDO.setUpdateUser(updateUser); + baseDO.setUpdateTime(updateTime); + } else { + // 未继承 BaseDO 的类,根据类中拥有的修改信息字段进行填充,不存在修改信息字段不进行填充 + this.fillFieldValue(metaObject, UPDATE_USER, updateUser, true); + this.fillFieldValue(metaObject, UPDATE_TIME, updateTime, true); + } + } + + /** + * 填充字段值 + * + * @param metaObject 元数据对象 + * @param fieldName 要填充的字段名 + * @param fillFieldValue 要填充的字段值 + * @param isOverride 如果字段值不为空,是否覆盖(true:覆盖;false:不覆盖) + */ + private void fillFieldValue(MetaObject metaObject, String fieldName, Object fillFieldValue, boolean isOverride) { + if (metaObject.hasSetter(fieldName)) { + Object fieldValue = metaObject.getValue(fieldName); + setFieldValByName(fieldName, null != fieldValue && !isOverride ? fieldValue : fillFieldValue, metaObject); + } + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/mybatis/MybatisPlusConfiguration.java b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/MybatisPlusConfiguration.java new file mode 100644 index 0000000..0b4da14 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/mybatis/MybatisPlusConfiguration.java @@ -0,0 +1,41 @@ +package top.wms.admin.common.config.mybatis; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.password.PasswordEncoder; +import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider; + +/** + * MyBatis Plus 配置 + * + * @author Charles7c + * @since 2022/12/22 19:51 + */ +@Configuration +public class MybatisPlusConfiguration { + + /** + * 元对象处理器配置(插入或修改时自动填充) + */ + @Bean + public MetaObjectHandler metaObjectHandler() { + return new MyBatisPlusMetaObjectHandler(); + } + + /** + * 数据权限用户上下文提供者 + */ + @Bean + public DataPermissionUserContextProvider dataPermissionUserContextProvider() { + return new DefaultDataPermissionUserContextProvider(); + } + + /** + * BCrypt 加/解密处理器 + */ + @Bean + public BCryptEncryptor bCryptEncryptor(PasswordEncoder passwordEncoder) { + return new BCryptEncryptor(passwordEncoder); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/properties/CaptchaProperties.java b/mes-common/src/main/java/top/mes/admin/common/config/properties/CaptchaProperties.java new file mode 100644 index 0000000..2ace23c --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/properties/CaptchaProperties.java @@ -0,0 +1,76 @@ +package top.wms.admin.common.config.properties; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 验证码配置属性 + * + * @author Charles7c + * @since 2022/12/11 13:35 + */ +@Data +@Component +@ConfigurationProperties(prefix = "captcha") +public class CaptchaProperties { + + /** + * 图形验证码过期时间 + */ + @Value("${continew-starter.captcha.graphic.expirationInMinutes}") + private long expirationInMinutes; + + /** + * 邮箱验证码配置 + */ + private CaptchaMail mail; + + /** + * 短信验证码配置 + */ + private CaptchaSms sms; + + /** + * 邮箱验证码配置 + */ + @Data + public static class CaptchaMail { + /** + * 内容长度 + */ + private int length; + + /** + * 过期时间 + */ + private long expirationInMinutes; + + /** + * 模板路径 + */ + private String templatePath; + } + + /** + * 短信验证码配置 + */ + @Data + public static class CaptchaSms { + /** + * 内容长度 + */ + private int length; + + /** + * 过期时间 + */ + private long expirationInMinutes; + + /** + * 模板 ID + */ + private String templateId; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/properties/RsaProperties.java b/mes-common/src/main/java/top/mes/admin/common/config/properties/RsaProperties.java new file mode 100644 index 0000000..5e6391c --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/properties/RsaProperties.java @@ -0,0 +1,27 @@ +package top.wms.admin.common.config.properties; + +import cn.hutool.extra.spring.SpringUtil; + +/** + * RSA 配置属性 + * + * @author Zheng Jie(ELADMIN) + * @author Charles7c + * @since 2022/12/21 20:21 + */ +public class RsaProperties { + + /** + * 私钥 + */ + public static final String PRIVATE_KEY; + public static final String PUBLIC_KEY; + + static { + PRIVATE_KEY = SpringUtil.getProperty("continew-starter.security.crypto.private-key"); + PUBLIC_KEY = SpringUtil.getProperty("continew-starter.security.crypto.public-key"); + } + + private RsaProperties() { + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/config/websocket/WebSocketClientServiceImpl.java b/mes-common/src/main/java/top/mes/admin/common/config/websocket/WebSocketClientServiceImpl.java new file mode 100644 index 0000000..9156b9c --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/config/websocket/WebSocketClientServiceImpl.java @@ -0,0 +1,28 @@ +package top.wms.admin.common.config.websocket; + +import cn.dev33.satoken.stp.StpUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.stereotype.Component; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.messaging.websocket.core.WebSocketClientService; + +/** + * 当前登录用户 Provider + * + * @author Charles7c + * @since 2024/6/4 22:13 + */ +@Component +public class WebSocketClientServiceImpl implements WebSocketClientService { + + @Override + public String getClientId(ServletServerHttpRequest request) { + HttpServletRequest servletRequest = request.getServletRequest(); + String token = servletRequest.getParameter("token"); + if (null == StpUtil.getLoginIdByToken(token)) { + throw new BusinessException("登录已过期,请重新登录"); + } + return token; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/constant/CacheConstants.java b/mes-common/src/main/java/top/mes/admin/common/constant/CacheConstants.java new file mode 100644 index 0000000..0f68752 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/constant/CacheConstants.java @@ -0,0 +1,65 @@ +package top.wms.admin.common.constant; + +import top.continew.starter.core.constant.StringConstants; + +/** + * 缓存相关常量 + * + * @author Charles7c + * @since 2022/12/22 19:30 + */ +public class CacheConstants { + + /** + * 分隔符 + */ + public static final String DELIMITER = StringConstants.COLON; + + /** + * 验证码键前缀 + */ + public static final String CAPTCHA_KEY_PREFIX = "CAPTCHA" + DELIMITER; + + /** + * 用户缓存键前缀 + */ + public static final String USER_KEY_PREFIX = "USER" + DELIMITER; + + /** + * 角色菜单缓存键前缀 + */ + public static final String ROLE_MENU_KEY_PREFIX = "ROLE_MENU" + DELIMITER; + + /** + * 字典缓存键前缀 + */ + public static final String DICT_KEY_PREFIX = "DICT" + DELIMITER; + + /** + * 参数缓存键前缀 + */ + public static final String OPTION_KEY_PREFIX = "OPTION" + DELIMITER; + + /** + * 仪表盘缓存键前缀 + */ + public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD" + DELIMITER; + + /** + * 用户密码错误次数缓存键前缀 + */ + public static final String USER_PASSWORD_ERROR_KEY_PREFIX = USER_KEY_PREFIX + "PASSWORD_ERROR" + DELIMITER; + + /** + * 数据导入临时会话key + */ + public static final String DATA_IMPORT_KEY = "SYSTEM" + DELIMITER + "DATA_IMPORT" + DELIMITER; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + private CacheConstants() { + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/constant/Constants.java b/mes-common/src/main/java/top/mes/admin/common/constant/Constants.java new file mode 100644 index 0000000..a95b92f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/constant/Constants.java @@ -0,0 +1,202 @@ +package top.wms.admin.common.constant; + +/** + * 通用常量信息 + * + * @author dcsoft + */ +public class Constants { + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 成功标记 + */ + public static final Integer SUCCESS = 200; + + /** + * 失败标记 + */ + public static final Integer FAIL = 500; + + /** + * 登录成功状态 + */ + public static final String LOGIN_SUCCESS_STATUS = "0"; + + /** + * 登录失败状态 + */ + public static final String LOGIN_FAIL_STATUS = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 验证码有效期(分钟) + */ + public static final long CAPTCHA_EXPIRATION = 2; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = {"com.dcsoft"}; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.dcsoft.common.core.utils.file"}; + + /** + * 0 + */ + public static final String ZERO = "0"; + + /** + * 1 + */ + public static final String ONE = "1"; + + /** + * 2 + */ + public static final String TWO = "2"; + + /** + * 5 + */ + public static final String FIVE = "5"; + + /** + * 200 + */ + public static final String CODE = "200"; + + /** + * OK + */ + public static final String OK = "ok"; + + /** + * 宇泛门禁设备开关门状态1开门 + */ + public static final Integer ACCESS_CONTROL_ONE = 1; + + /** + * 宇泛门禁设备开关门状态2关门 + */ + public static final Integer ACCESS_CONTROL_TWO = 2; + + /** + * + */ + public static final String TTS_MOD_CONTENT = "未授权,请联系管理员"; + + /** + * + */ + public static final String DISPLAY_MOD_CONTENT = "未授权,请联系管理员"; + + /** + * 梯控结束时间 + */ + public static final String END_TIME = "2099-12-31 23:59:59"; + + /** + * 梯控起始可用时间 + */ + public static final String START_DAY_TIME = "00:00:00"; + + /** + * 梯控结束可用时间 + */ + public static final String END_DAY_TIME = "23:59:59"; + + /** + * 梯控星期 + */ + public static final String WEEK = "1 1 1 1 1 1 1"; + + public static final String CONTENT = "{\n" + " \"touser\": \"%\", \n" + " \"template_id\": \"wODO_BDj8n1grelUSGTNvpZbSnFdhzC3odWHek0brSM\",\n" + " \"url\": \"#\",\n" + " \"data\": {\n" + " \"first\": {\n" + " \"value\": \"广州腾讯科技有限公司\"\n" + " },\n" + " \"keyword1\": {\n" + " \"value\": \"广州腾讯科技有限公司\"\n" + " },\n" + " \"keyword2\": {\n" + " \"value\": \"2019年8月8日\"\n" + " }\n" + " }\n" + "}"; + +} diff --git a/mes-common/src/main/java/top/mes/admin/common/constant/ContainerConstants.java b/mes-common/src/main/java/top/mes/admin/common/constant/ContainerConstants.java new file mode 100644 index 0000000..2dd92eb --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/constant/ContainerConstants.java @@ -0,0 +1,73 @@ +package top.wms.admin.common.constant; + +/** + * 数据源容器相关常量(Crane4j 数据填充组件使用) + * + * @author Charles7c + * @since 2024/1/20 12:33 + */ +public class ContainerConstants { + + /** + * 用户昵称 + */ + public static final String USER_NICKNAME = "UserNickname"; + + /** + * 文件信息 + */ + public static final String FILE_INFO = "FileInfo"; + + /** + * 用户角色 ID 列表 + */ + public static final String USER_ROLE_ID_LIST = "UserRoleIdList"; + + /** + * 用户角色名称列表 + */ + public static final String USER_ROLE_NAME_LIST = "UserRoleNameList"; + + /** + * 用户列表 + */ + public static final String USER_NAME_LIST = "UserNameList"; + + /** + * 用户名称 + */ + public static final String USER_NAME = "UserName"; + + /** + * 部门名称 + */ + public static final String BRANCH_NAME = "BranchName"; + + /** + * 空间名称 + */ + public static final String SPACE_NAME = "SpaceName"; + + /** + * 产品名称图片 + */ + public static final String PRODUCT_NAME_AVATAR = "ProductNameAvatar"; + + /** + * 设备名称 + */ + public static final String EQUIPMENT_NAME = "EquipmentName"; + + /** + * 人员名称 + */ + public static final String PEOPLE_NAME = "PeopleName"; + + /** + * 规则名称 + */ + public static final String RULE_NAME = "RuleName"; + + private ContainerConstants() { + } +} \ No newline at end of file diff --git a/mes-common/src/main/java/top/mes/admin/common/constant/RegexConstants.java b/mes-common/src/main/java/top/mes/admin/common/constant/RegexConstants.java new file mode 100644 index 0000000..a9dc00f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/constant/RegexConstants.java @@ -0,0 +1,53 @@ +package top.wms.admin.common.constant; + +/** + * 正则相关常量 + * + * @author Charles7c + * @since 2023/1/10 20:06 + */ +public class RegexConstants { + + /** + * 用户名正则(用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头) + */ + public static final String USERNAME = "^[a-zA-Z][a-zA-Z0-9_]{3,64}$"; + + /** + * 密码正则模板(密码长度为 min-max 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字) + */ + public static final String PASSWORD_TEMPLATE = "^(?=.*\\d)(?=.*[a-z]).{%s,%s}$"; + + /** + * 密码正则(密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字) + */ + public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z]).{8,32}$"; + + /** + * 特殊字符正则 + */ + public static final String SPECIAL_CHARACTER = "[-_`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\n|\\r|\\t"; + + /** + * 通用编码正则(长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头) + */ + public static final String GENERAL_CODE = "^[a-zA-Z][a-zA-Z0-9_]{1,29}$"; + + /** + * 通用名称正则(长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线) + */ + public static final String GENERAL_NAME = "^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{2,30}$"; + + /** + * 包名正则(可以包含大小写字母、数字、下划线,每一级包名不能以数字开头) + */ + public static final String PACKAGE_NAME = "^(?:[a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*$"; + + /** + * 金额正则表达式:纯数字,保留两位小数 + */ + public static final String MONEY = "^\\d+(\\.\\d{2})?$"; + + private RegexConstants() { + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/constant/SysConstants.java b/mes-common/src/main/java/top/mes/admin/common/constant/SysConstants.java new file mode 100644 index 0000000..43e85d5 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/constant/SysConstants.java @@ -0,0 +1,68 @@ +package top.wms.admin.common.constant; + +/** + * 系统相关常量 + * + * @author Charles7c + * @since 2023/2/9 22:11 + */ +public class SysConstants { + + /** + * 否 + */ + public static final Integer NO = 0; + + /** + * 是 + */ + public static final Integer YES = 1; + + /** + * 超管用户 ID + */ + public static final Long SUPER_USER_ID = 1L; + + /** + * 顶级部门 ID + */ + public static final Long SUPER_DEPT_ID = 1L; + + /** + * 顶级父 ID + */ + public static final Long SUPER_PARENT_ID = 0L; + + /** + * 超管角色编码 + */ + public static final String SUPER_ROLE_CODE = "admin"; + + /** + * 超管角色 ID + */ + public static final Long SUPER_ROLE_ID = 1L; + + /** + * 全部权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 登录 URI + */ + public static final String LOGIN_URI = "/auth/login"; + + /** + * 登出 URI + */ + public static final String LOGOUT_URI = "/auth/logout"; + + /** + * 描述类字段后缀 + */ + public static final String DESCRIPTION_FIELD_SUFFIX = "String"; + + private SysConstants() { + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/constant/UiConstants.java b/mes-common/src/main/java/top/mes/admin/common/constant/UiConstants.java new file mode 100644 index 0000000..1612b01 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/constant/UiConstants.java @@ -0,0 +1,38 @@ +package top.wms.admin.common.constant; + +/** + * UI 相关常量 + * + * @author Charles7c + * @since 2023/9/17 14:12 + */ +public class UiConstants { + + /** + * 主色(极致蓝) + */ + public static final String COLOR_PRIMARY = "arcoblue"; + + /** + * 成功色(仙野绿) + */ + public static final String COLOR_SUCCESS = "green"; + + /** + * 警告色(活力橙) + */ + public static final String COLOR_WARNING = "orangered"; + + /** + * 错误色(浪漫红) + */ + public static final String COLOR_ERROR = "red"; + + /** + * 默认色(中性灰) + */ + public static final String COLOR_DEFAULT = "gray"; + + private UiConstants() { + } +} \ No newline at end of file diff --git a/mes-common/src/main/java/top/mes/admin/common/context/RoleContext.java b/mes-common/src/main/java/top/mes/admin/common/context/RoleContext.java new file mode 100644 index 0000000..c2763a7 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/context/RoleContext.java @@ -0,0 +1,43 @@ +package top.wms.admin.common.context; + +import lombok.Data; +import lombok.NoArgsConstructor; +import top.wms.admin.common.enums.DataScopeEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 角色上下文 + * + * @author Charles7c + * @since 2023/3/7 22:08 + */ +@Data +@NoArgsConstructor +public class RoleContext implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + + /** + * 角色编码 + */ + private String code; + + /** + * 数据权限 + */ + private DataScopeEnum dataScope; + + public RoleContext(Long id, String code, DataScopeEnum dataScope) { + this.id = id; + this.code = code; + this.dataScope = dataScope; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/context/UserContext.java b/mes-common/src/main/java/top/mes/admin/common/context/UserContext.java new file mode 100644 index 0000000..5bcd49c --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/context/UserContext.java @@ -0,0 +1,116 @@ +package top.wms.admin.common.context; + +import cn.hutool.core.collection.CollUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import top.wms.admin.common.constant.SysConstants; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 用户上下文 + * + * @author Charles7c + * @since 2024/10/9 20:29 + */ +@Data +@NoArgsConstructor +public class UserContext implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + private Long id; + + /** + * 用户名 + */ + private String username; + + /** + * 部门 ID + */ + private Long deptId; + + /** + * 最后一次修改密码时间 + */ + private LocalDateTime pwdResetTime; + + /** + * 登录时系统设置的密码过期天数 + */ + private Integer passwordExpirationDays; + + /** + * 权限码集合 + */ + private Set permissions; + + /** + * 角色编码集合 + */ + private Set roleCodes; + + /** + * 角色集合 + */ + private Set roles; + + /** + * 终端类型 + */ + private String clientType; + + /** + * 终端 ID + */ + private String clientId; + + public UserContext(Set permissions, Set roles, Integer passwordExpirationDays) { + this.permissions = permissions; + this.setRoles(roles); + this.passwordExpirationDays = passwordExpirationDays; + } + + public void setRoles(Set roles) { + this.roles = roles; + this.roleCodes = roles.stream().map(RoleContext::getCode).collect(Collectors.toSet()); + } + + /** + * 是否为管理员 + * + * @return true:是;false:否 + */ + public boolean isAdmin() { + if (CollUtil.isEmpty(roleCodes)) { + return false; + } + return roleCodes.contains(SysConstants.SUPER_ROLE_CODE); + } + + /** + * 密码是否已过期 + * + * @return 是否过期 + */ + public boolean isPasswordExpired() { + // 永久有效 + if (this.passwordExpirationDays == null || this.passwordExpirationDays <= SysConstants.NO) { + return false; + } + // 初始密码(第三方登录用户)暂不提示修改 + if (this.pwdResetTime == null) { + return false; + } + return this.pwdResetTime.plusDays(this.passwordExpirationDays).isBefore(LocalDateTime.now()); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/context/UserContextHolder.java b/mes-common/src/main/java/top/mes/admin/common/context/UserContextHolder.java new file mode 100644 index 0000000..c8cba13 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/context/UserContextHolder.java @@ -0,0 +1,167 @@ +package top.wms.admin.common.context; + +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.extra.spring.SpringUtil; +import top.wms.admin.common.service.CommonUserService; +import top.continew.starter.core.util.ExceptionUtils; + +/** + * 用户上下文 Holder + * + * @author Charles7c + * @since 2022/12/24 12:58 + */ +public class UserContextHolder { + + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + private static final ThreadLocal EXTRA_CONTEXT_HOLDER = new ThreadLocal<>(); + + private UserContextHolder() { + } + + /** + * 设置上下文 + * + * @param context 上下文 + */ + public static void setContext(UserContext context) { + setContext(context, true); + } + + /** + * 设置上下文 + * + * @param context 上下文 + * @param isUpdate 是否更新 + */ + public static void setContext(UserContext context, boolean isUpdate) { + CONTEXT_HOLDER.set(context); + if (isUpdate) { + StpUtil.getSessionByLoginId(context.getId()).set(SaSession.USER, context); + } + } + + /** + * 获取上下文 + * + * @return 上下文 + */ + public static UserContext getContext() { + UserContext context = CONTEXT_HOLDER.get(); + if (null == context) { + context = StpUtil.getSession().getModel(SaSession.USER, UserContext.class); + CONTEXT_HOLDER.set(context); + } + return context; + } + + /** + * 获取指定用户的上下文 + * + * @param userId 用户 ID + * @return 上下文 + */ + public static UserContext getContext(Long userId) { + SaSession session = StpUtil.getSessionByLoginId(userId, false); + if (null == session) { + return null; + } + return session.getModel(SaSession.USER, UserContext.class); + } + + /** + * 设置额外上下文 + * + * @param context 额外上下文 + */ + public static void setExtraContext(UserExtraContext context) { + EXTRA_CONTEXT_HOLDER.set(context); + } + + /** + * 获取额外上下文 + * + * @return 额外上下文 + */ + public static UserExtraContext getExtraContext() { + UserExtraContext context = EXTRA_CONTEXT_HOLDER.get(); + if (null == context) { + context = getExtraContext(StpUtil.getTokenValue()); + EXTRA_CONTEXT_HOLDER.set(context); + } + return context; + } + + /** + * 获取额外上下文 + * + * @param token 令牌 + * @return 额外上下文 + */ + public static UserExtraContext getExtraContext(String token) { + UserExtraContext context = new UserExtraContext(); + context.setIp(Convert.toStr(StpUtil.getExtra(token, "ip"))); + context.setAddress(Convert.toStr(StpUtil.getExtra(token, "address"))); + context.setBrowser(Convert.toStr(StpUtil.getExtra(token, "browser"))); + context.setOs(Convert.toStr(StpUtil.getExtra(token, "os"))); + context.setLoginTime(Convert.toLocalDateTime(StpUtil.getExtra(token, "loginTime"))); + return context; + } + + /** + * 清除上下文 + */ + public static void clearContext() { + CONTEXT_HOLDER.remove(); + EXTRA_CONTEXT_HOLDER.remove(); + } + + /** + * 获取用户 ID + * + * @return 用户 ID + */ + public static Long getUserId() { + return ExceptionUtils.exToNull(() -> getContext().getId()); + } + + /** + * 获取用户名 + * + * @return 用户名 + */ + public static String getUsername() { + return ExceptionUtils.exToNull(() -> getContext().getUsername()); + } + + /** + * 获取用户昵称 + * + * @return 用户昵称 + */ + public static String getNickname() { + return getNickname(getUserId()); + } + + /** + * 获取用户昵称 + * + * @param userId 登录用户 ID + * @return 用户昵称 + */ + public static String getNickname(Long userId) { + return ExceptionUtils.exToNull(() -> SpringUtil.getBean(CommonUserService.class).getNicknameById(userId)); + } + + /** + * 是否为管理员 + * + * @return 是否为管理员 + */ + public static boolean isAdmin() { + StpUtil.checkLogin(); + return getContext().isAdmin(); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/context/UserExtraContext.java b/mes-common/src/main/java/top/mes/admin/common/context/UserExtraContext.java new file mode 100644 index 0000000..1bda77a --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/context/UserExtraContext.java @@ -0,0 +1,61 @@ +package top.wms.admin.common.context; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.Data; +import lombok.NoArgsConstructor; +import top.continew.starter.core.util.ExceptionUtils; +import top.continew.starter.core.util.IpUtils; +import top.continew.starter.web.util.ServletUtils; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户额外上下文 + * + * @author Charles7c + * @since 2024/10/9 20:29 + */ +@Data +@NoArgsConstructor +public class UserExtraContext implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * IP + */ + private String ip; + + /** + * IP 归属地 + */ + private String address; + + /** + * 浏览器 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 登录时间 + */ + private LocalDateTime loginTime; + + public UserExtraContext(HttpServletRequest request) { + this.ip = JakartaServletUtil.getClientIP(request); + this.address = ExceptionUtils.exToNull(() -> IpUtils.getIpv4Address(this.ip)); + this.setBrowser(ServletUtils.getBrowser(request)); + this.setLoginTime(LocalDateTime.now()); + this.setOs(StrUtil.subBefore(ServletUtils.getOs(request), " or", false)); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/controller/BaseController.java b/mes-common/src/main/java/top/mes/admin/common/controller/BaseController.java new file mode 100644 index 0000000..238c494 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/controller/BaseController.java @@ -0,0 +1,51 @@ +package top.wms.admin.common.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.sign.SaSignTemplate; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.text.CharSequenceUtil; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.extension.crud.annotation.CrudApi; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.controller.AbstractBaseController; +import top.continew.starter.extension.crud.enums.Api; +import top.continew.starter.extension.crud.service.BaseService; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * 控制器基类 + * + * @param 业务接口 + * @param 列表类型 + * @param 详情类型 + * @param 查询条件 + * @param 创建或修改参数类型 + * @author Charles7c + * @since 2024/12/6 20:30 + */ +public class BaseController, L, D, Q, C> extends AbstractBaseController { + + @Override + public void preHandle(CrudApi crudApi, Object[] args, Method targetMethod, Class targetClass) throws Exception { + SaRequest saRequest = SaHolder.getRequest(); + List paramNames = saRequest.getParamNames(); + if (paramNames.stream().anyMatch(SaSignTemplate.sign::equals)) { + return; + } + if (AnnotationUtil.hasAnnotation(targetMethod, SaIgnore.class) || AnnotationUtil + .hasAnnotation(targetClass, SaIgnore.class)) { + return; + } + CrudRequestMapping crudRequestMapping = targetClass.getDeclaredAnnotation(CrudRequestMapping.class); + String path = crudRequestMapping.value(); + String prefix = String.join(StringConstants.COLON, CharSequenceUtil.splitTrim(path, StringConstants.SLASH)); + Api api = crudApi.value(); + String apiName = Api.PAGE.equals(api) || Api.TREE.equals(api) ? Api.LIST.name() : api.name(); + StpUtil.checkPermission("%s:%s".formatted(prefix, apiName.toLowerCase())); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/CommandTypeEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/CommandTypeEnum.java new file mode 100644 index 0000000..09af9a9 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/CommandTypeEnum.java @@ -0,0 +1,63 @@ +package top.wms.admin.common.enums; + +/** + * DPA6024V-2T-1.0 数字控制器命令类型枚举 + * 定义控制器支持的指令字 + */ +public enum CommandTypeEnum { + + /** + * 打开对应通道 (指令字: 1) + */ + ON('1'), + + /** + * 关闭对应通道 (指令字: 2) + */ + OFF('2'), + + /** + * 设置对应通道亮度参数 (指令字: 3) + */ + SET_BRIGHTNESS('3'), + + /** + * 读出对应通道亮度参数 (指令字: 4) + */ + READ('4'); + + private final char commandCode; + + CommandTypeEnum(char commandCode) { + this.commandCode = commandCode; + } + + /** + * 获取指令字字符 + * + * @return 指令字 (如 '1', '2', '3', '4') + */ + public char getCommandCode() { + return commandCode; + } + + /** + * 根据指令字获取对应的命令类型 + * + * @param commandCode 指令字字符 + * @return 对应的CommandType,找不到返回null + */ + public static CommandTypeEnum fromCommandCode(char commandCode) { + for (CommandTypeEnum type : values()) { + if (type.commandCode == commandCode) { + return type; + } + } + return null; + } + + @Override + public String toString() { + return String.valueOf(commandCode); + } +} \ No newline at end of file diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/CommonExceptionEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/CommonExceptionEnum.java new file mode 100644 index 0000000..0b83477 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/CommonExceptionEnum.java @@ -0,0 +1,22 @@ +package top.wms.admin.common.enums; + +public enum CommonExceptionEnum { + RULE_ERROR(1000, "您所所在得部门所属规则未绑定设备"), REQUEST_ERROR(1001, "请求异常"), PEOPLE_ERROR(1002, "您的信息未录入"), + QR_CODE_ERROR(1003, "二维码过期"), DEVICE_INITIALIZE_ERROR(1004, "设备序列号不能为空"),; + + private Integer errCode; + private String errMsg; + + CommonExceptionEnum(Integer errCode, String errMsg) { + this.errCode = errCode; + this.errMsg = errMsg; + } + + public Integer getCode() { + return errCode; + } + + public String getMessage() { + return errMsg; + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/DataScopeEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/DataScopeEnum.java new file mode 100644 index 0000000..774081f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/DataScopeEnum.java @@ -0,0 +1,44 @@ +package top.wms.admin.common.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 数据权限枚举 + * + * @author Charles7c + * @since 2023/2/8 22:58 + */ +@Getter +@RequiredArgsConstructor +public enum DataScopeEnum implements BaseEnum { + + /** + * 全部数据权限 + */ + ALL(1, "全部数据权限"), + + /** + * 本部门及以下数据权限 + */ + DEPT_AND_CHILD(2, "本部门及以下数据权限"), + + /** + * 本部门数据权限 + */ + DEPT(3, "本部门数据权限"), + + /** + * 仅本人数据权限 + */ + SELF(4, "仅本人数据权限"), + + /** + * 自定义数据权限 + */ + CUSTOM(5, "自定义数据权限"),; + + private final Integer value; + private final String description; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/DisEnableStatusEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/DisEnableStatusEnum.java new file mode 100644 index 0000000..37d193f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/DisEnableStatusEnum.java @@ -0,0 +1,31 @@ +package top.wms.admin.common.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 启用/禁用状态枚举 + * + * @author Charles7c + * @since 2022/12/29 22:38 + */ +@Getter +@RequiredArgsConstructor +public enum DisEnableStatusEnum implements BaseEnum { + + /** + * 启用 + */ + ENABLE(1, "启用", UiConstants.COLOR_SUCCESS), + + /** + * 禁用 + */ + DISABLE(2, "禁用", UiConstants.COLOR_ERROR),; + + private final Integer value; + private final String description; + private final String color; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/GenderEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/GenderEnum.java new file mode 100644 index 0000000..9a3e20a --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/GenderEnum.java @@ -0,0 +1,34 @@ +package top.wms.admin.common.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 性别枚举 + * + * @author Charles7c + * @since 2022/12/29 21:59 + */ +@Getter +@RequiredArgsConstructor +public enum GenderEnum implements BaseEnum { + + /** + * 未知 + */ + UNKNOWN(0, "未知"), + + /** + * 男 + */ + MALE(1, "男"), + + /** + * 女 + */ + FEMALE(2, "女"),; + + private final Integer value; + private final String description; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/SuccessFailureStatusEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/SuccessFailureStatusEnum.java new file mode 100644 index 0000000..47af52f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/SuccessFailureStatusEnum.java @@ -0,0 +1,31 @@ +package top.wms.admin.common.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 成功/失败状态枚举 + * + * @author Charles7c + * @since 2023/2/26 21:35 + */ +@Getter +@RequiredArgsConstructor +public enum SuccessFailureStatusEnum implements BaseEnum { + + /** + * 成功 + */ + SUCCESS(0, "成功", UiConstants.COLOR_SUCCESS), + + /** + * 失败 + */ + FAILURE(1, "失败", UiConstants.COLOR_ERROR),; + + private final Integer value; + private final String description; + private final String color; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/enums/YesNoEnum.java b/mes-common/src/main/java/top/mes/admin/common/enums/YesNoEnum.java new file mode 100644 index 0000000..3585d28 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/enums/YesNoEnum.java @@ -0,0 +1,46 @@ +package top.wms.admin.common.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; +import top.wms.admin.common.constant.UiConstants; + +/** + * 成功/失败状态枚举 + * + * @author Charles7c + * @since 2023/2/26 21:35 + */ +@Getter +@RequiredArgsConstructor +public enum YesNoEnum implements BaseEnum { + + /** + * 否 + */ + NO(0, "否", UiConstants.COLOR_ERROR), + + /** + * 是 + */ + YES(1, "是", UiConstants.COLOR_SUCCESS),; + + private final Integer value; + private final String description; + private final String color; + + /** + * 根据描述获取枚举常量 + * + * @param description 描述 + * @return 枚举常量 + */ + public static YesNoEnum getByDescription(String description) { + for (YesNoEnum enumValue : values()) { + if (enumValue.getDescription().equals(description)) { + return enumValue; + } + } + throw new IllegalArgumentException("No enum constant with description: " + description); + } +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseCreateDO.java b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseCreateDO.java new file mode 100644 index 0000000..bcef4b0 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseCreateDO.java @@ -0,0 +1,38 @@ +package top.wms.admin.common.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import top.continew.starter.extension.crud.model.entity.BaseIdDO; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** + * 实体类基类 + * + *

+ * 通用字段:创建人、创建时间 + *

+ * + * @author Charles7c + * @since 2025/1/12 23:00 + */ +@Data +public class BaseCreateDO extends BaseIdDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 创建人 + */ + @TableField(fill = FieldFill.INSERT) + private Long createUser; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseDO.java b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseDO.java new file mode 100644 index 0000000..f50415b --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseDO.java @@ -0,0 +1,46 @@ +package top.wms.admin.common.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import top.continew.starter.extension.crud.model.entity.BaseIdDO; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** + * 实体类基类 + * + * @author Charles7c + * @since 2025/1/12 23:00 + */ +@Data +public class BaseDO extends BaseIdDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 创建人 + */ + @TableField(fill = FieldFill.INSERT) + private Long createUser; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改人 + */ + @TableField(fill = FieldFill.UPDATE) + private Long updateUser; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.UPDATE) + private LocalDateTime updateTime; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseStrIdDO.java b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseStrIdDO.java new file mode 100644 index 0000000..dd433f3 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseStrIdDO.java @@ -0,0 +1,51 @@ +package top.wms.admin.common.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 实体类基类 + * + * @author Charles7c + * @since 2025/1/12 23:00 + */ +@Data +public class BaseStrIdDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 创建人 + */ + @TableField(fill = FieldFill.INSERT) + private Long createUser; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改人 + */ + @TableField(fill = FieldFill.UPDATE) + private Long updateUser; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.UPDATE) + private LocalDateTime updateTime; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseUpdateDO.java b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseUpdateDO.java new file mode 100644 index 0000000..00fa7b6 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/entity/BaseUpdateDO.java @@ -0,0 +1,39 @@ +package top.wms.admin.common.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.io.Serial; +import java.time.LocalDateTime; + +import top.continew.starter.extension.crud.model.entity.BaseIdDO; + +/** + * 实体类基类 + * + *

+ * 通用字段:创建人、创建时间 + *

+ * + * @author Charles7c + * @since 2025/1/12 23:00 + */ +@Data +public class BaseUpdateDO extends BaseIdDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 修改人 + */ + @TableField(fill = FieldFill.UPDATE) + private Long updateUser; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.UPDATE) + private LocalDateTime updateTime; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/req/CommonStatusUpdateReq.java b/mes-common/src/main/java/top/mes/admin/common/model/req/CommonStatusUpdateReq.java new file mode 100644 index 0000000..36fa00d --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/req/CommonStatusUpdateReq.java @@ -0,0 +1,26 @@ +package top.wms.admin.common.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serializable; + +/** + * 修改状态请求参数 + * + * @author Charles7c + * @since 2025/3/4 20:09 + */ +@Data +@Schema(description = "修改状态请求参数") +public class CommonStatusUpdateReq implements Serializable { + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @NotNull(message = "状态非法") + private DisEnableStatusEnum status; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseDetailResp.java b/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseDetailResp.java new file mode 100644 index 0000000..f477796 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseDetailResp.java @@ -0,0 +1,51 @@ +package top.wms.admin.common.model.resp; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.annotation.Mapping; +import cn.crane4j.annotation.condition.ConditionOnPropertyNotNull; +import com.alibaba.excel.annotation.ExcelIgnore; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.constant.ContainerConstants; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** + * 详情响应基类 + * + * @author Charles7c + * @since 2024/12/27 20:32 + */ +@Data +public class BaseDetailResp extends BaseResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 修改人 + */ + @JsonIgnore + @ConditionOnPropertyNotNull + @Assemble(container = ContainerConstants.USER_NICKNAME, props = @Mapping(ref = "updateUserString")) + @ExcelIgnore + private Long updateUser; + + /** + * 修改人 + */ + @Schema(description = "修改人", example = "李四") + // @ExcelProperty(value = "修改人", order = Integer.MAX_VALUE - 2) + @ExcelIgnore + private String updateUserString; + + /** + * 修改时间 + */ + @Schema(description = "修改时间", example = "2023-08-08 08:08:08", type = "string") + // @ExcelProperty(value = "修改时间", order = Integer.MAX_VALUE - 1) + @ExcelIgnore + private LocalDateTime updateTime; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseResp.java b/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseResp.java new file mode 100644 index 0000000..16a9cf3 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseResp.java @@ -0,0 +1,67 @@ +package top.wms.admin.common.model.resp; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.annotation.Mapping; +import com.alibaba.excel.annotation.ExcelIgnore; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.constant.ContainerConstants; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 响应参数基类 + * + * @author Charles7c + * @since 2024/12/27 20:32 + */ +@Data +public class BaseResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + // @ExcelProperty(value = "ID", order = 1) + @ExcelIgnore + private Long id; + + /** + * 创建人 + */ + @JsonIgnore + @Assemble(container = ContainerConstants.USER_NICKNAME, props = @Mapping(ref = "createUserString")) + @ExcelIgnore + private Long createUser; + + /** + * 创建人 + */ + @Schema(description = "创建人", example = "超级管理员") + // @ExcelProperty(value = "创建人", order = Integer.MAX_VALUE - 4) + @ExcelIgnore + private String createUserString; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + // @ExcelProperty(value = "创建时间", order = Integer.MAX_VALUE - 3) + @ExcelIgnore + private LocalDateTime createTime; + + /** + * 是否禁用修改 + */ + @Schema(description = "是否禁用修改", example = "true") + @JsonInclude(JsonInclude.Include.NON_NULL) + @ExcelIgnore + private Boolean disabled; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseStrIdResp.java b/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseStrIdResp.java new file mode 100644 index 0000000..044e0ba --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/model/resp/BaseStrIdResp.java @@ -0,0 +1,62 @@ +package top.wms.admin.common.model.resp; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.annotation.Mapping; +import com.alibaba.excel.annotation.ExcelProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.constant.ContainerConstants; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 响应参数基类 + * + * @author Charles7c + * @since 2024/12/27 20:32 + */ +@Data +public class BaseStrIdResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + @ExcelProperty(value = "ID", order = 1) + private String id; + + /** + * 创建人 + */ + @JsonIgnore + @Assemble(container = ContainerConstants.USER_NICKNAME, props = @Mapping(ref = "createUserString")) + private Long createUser; + + /** + * 创建人 + */ + @Schema(description = "创建人", example = "超级管理员") + @ExcelProperty(value = "创建人", order = Integer.MAX_VALUE - 4) + private String createUserString; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + @ExcelProperty(value = "创建时间", order = Integer.MAX_VALUE - 3) + private LocalDateTime createTime; + + /** + * 是否禁用修改 + */ + @Schema(description = "是否禁用修改", example = "true") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean disabled; +} diff --git a/mes-common/src/main/java/top/mes/admin/common/service/CommonUserService.java b/mes-common/src/main/java/top/mes/admin/common/service/CommonUserService.java new file mode 100644 index 0000000..018d17f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/service/CommonUserService.java @@ -0,0 +1,27 @@ +package top.wms.admin.common.service; + +import cn.crane4j.annotation.ContainerMethod; +import cn.crane4j.annotation.MappingType; +import top.wms.admin.common.constant.ContainerConstants; + +/** + * 公共用户业务接口 + * + * @author Charles7c + * @since 2025/1/9 20:17 + */ +public interface CommonUserService { + + /** + * 根据 ID 查询昵称 + * + *

+ * 数据填充容器 {@link ContainerConstants#USER_NICKNAME} + *

+ * + * @param id ID + * @return 昵称 + */ + @ContainerMethod(namespace = ContainerConstants.USER_NICKNAME, type = MappingType.ORDER_OF_KEYS) + String getNicknameById(Long id); +} diff --git a/mes-common/src/main/java/top/mes/admin/common/util/ImageToBase64Utils.java b/mes-common/src/main/java/top/mes/admin/common/util/ImageToBase64Utils.java new file mode 100644 index 0000000..99f9b2f --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/util/ImageToBase64Utils.java @@ -0,0 +1,148 @@ +package top.wms.admin.common.util; + +import org.apache.commons.codec.binary.Base64; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +public class ImageToBase64Utils { + /** + * 本地图片转base64 + */ + public static String getImgFileToBase642(String imgFile) { + //将图片文件转化为字节数组字符串,并对其进行Base64编码处理 + byte[] buffer = null; + //读取图片字节数组 + try (InputStream inputStream = new FileInputStream(imgFile);) { + int count = 0; + while (count == 0) { + count = inputStream.available(); + } + buffer = new byte[count]; + inputStream.read(buffer); + } catch (IOException e) { + e.printStackTrace(); + } + // 对字节数组Base64编码 + return new String(Base64.encodeBase64(buffer)); + } + + /** + * 网络图片转base64 + */ + public static String getImgUrlToBase64(String imgUrl) { + byte[] buffer = null; + InputStream inputStream = null; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) { + // 创建URL + URL url = new URL(imgUrl); + // 创建链接 + HttpURLConnection conn = (HttpURLConnection)url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + inputStream = conn.getInputStream(); + // 将内容读取内存中 + buffer = new byte[1024]; + int len = -1; + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + buffer = outputStream.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + // 关闭inputStream流 + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + // 对字节数组Base64编码 + return new String(Base64.encodeBase64(buffer)); + } + + /** + * 本地或网络图片转base64 + */ + public static String getImgStrToBase64(String imgStr) { + InputStream inputStream = null; + byte[] buffer = null; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) { + //判断网络链接图片文件/本地目录图片文件 + if (imgStr.startsWith("http://") || imgStr.startsWith("https://")) { + // 创建URL + URL url = new URL(imgStr); + // 创建链接 + HttpURLConnection conn = (HttpURLConnection)url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + inputStream = conn.getInputStream(); + // 将内容读取内存中 + buffer = new byte[1024]; + int len = -1; + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + buffer = outputStream.toByteArray(); + } else { + inputStream = new FileInputStream(imgStr); + int count = 0; + while (count == 0) { + count = inputStream.available(); + } + buffer = new byte[count]; + inputStream.read(buffer); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + // 关闭inputStream流 + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // 对字节数组Base64编码 + + return new String(Base64.encodeBase64(buffer)); + } + + /** + * 对字节数组字符串进行Base64解码并生成图片 + * + * @param imgStr 图片数据 + * @param imgFilePath 保存图片全路径地址 + * @return + */ + public static boolean generateImage(String imgStr, String imgFilePath) { + // + if (imgStr == null) //图像数据为空 + return false; + try { + //Base64解码 + byte[] b = Base64.decodeBase64(imgStr); + for (int i = 0; i < b.length; ++i) { + if (b[i] < 0) {//调整异常数据 + b[i] += 256; + } + } + //生成jpeg图片 + OutputStream out = new FileOutputStream(imgFilePath); + out.write(b); + out.flush(); + out.close(); + return true; + } catch (Exception e) { + return false; + } + } + +} diff --git a/mes-common/src/main/java/top/mes/admin/common/util/PictureUtils.java b/mes-common/src/main/java/top/mes/admin/common/util/PictureUtils.java new file mode 100644 index 0000000..d2df118 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/util/PictureUtils.java @@ -0,0 +1,155 @@ +package top.wms.admin.common.util; + +import net.coobird.thumbnailator.Thumbnails; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import top.continew.starter.core.exception.BusinessException; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; + +// java将图片的url转换成File,File转换成二进制流byte +public class PictureUtils { + //将Url转换为File + public static File UrltoFile(String url) throws Exception { + HttpURLConnection httpUrl = (HttpURLConnection)new URL(url).openConnection(); + httpUrl.connect(); + InputStream ins = httpUrl.getInputStream(); + File file = new File(System.getProperty("java.io.tmpdir") + File.separator + "xie.jpg"); + if (file.exists()) { + file.delete();//如果缓存中存在该文件就删除 + } + OutputStream os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + return file; + + } + + //将File对象转换为byte[]的形式 + public static byte[] FileTobyte(File file) { + FileInputStream fileInputStream = null; + byte[] imgData = null; + + try { + + imgData = new byte[(int)file.length()]; + + //read file into bytes[] + fileInputStream = new FileInputStream(file); + fileInputStream.read(imgData); + + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + return imgData; + } + + /** + * url转变为 MultipartFile对象 + * + * @param imageUrl 网络地址链接 + * @param fileName 文件名 + * @return + * @throws Exception + */ + public static MultipartFile createMultipartFile(String imageUrl, String fileName) { + try { + // 将在线图片地址转换为URL对象 + URL url = new URL(imageUrl); + // 打开URL连接 + URLConnection connection = url.openConnection(); + // 转换为HttpURLConnection对象 + HttpURLConnection httpURLConnection = (HttpURLConnection)connection; + // 获取输入流 + InputStream inputStream = httpURLConnection.getInputStream(); + // 读取输入流中的数据,并保存到字节数组中 + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); + } + // 将字节数组转换为字节数组 + byte[] bytes = byteArrayOutputStream.toByteArray(); + // 创建ByteArrayInputStream对象,将字节数组传递给它 + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + // 创建MultipartFile对象,将ByteArrayInputStream对象作为构造函数的参数 + return new MockMultipartFile(fileName, fileName + ".jpg", "image/jpg", byteArrayInputStream); + } catch (IOException ex) { + throw new BusinessException("附件无效"); + } + } + + /** + * 根据文件名获取contentType + */ + private static String getContentType(String fileName) { + // 使用Java标准库的方法替代Hutool的StrUtil + String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + Map contentTypeMap = new HashMap<>(); + contentTypeMap.put("jpg", "image/jpeg"); + contentTypeMap.put("jpeg", "image/jpeg"); + contentTypeMap.put("png", "image/png"); + contentTypeMap.put("gif", "image/gif"); + contentTypeMap.put("bmp", "image/bmp"); + // 可以根据需要添加更多的文件类型 + return contentTypeMap.getOrDefault(extension, "application/octet-stream"); + } + + /** + * 直接从输入流压缩图片并转换为MultipartFile + * + * @param inputStream 图片输入流 + * @param originalFilename 原始文件名 + * @param quality 压缩质量 (0.0-1.0) + * @return MultipartFile对象 + */ + public static MultipartFile compressImage(InputStream inputStream, + String originalFilename, + float quality) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + // 直接将压缩后的图片写入内存流 + Thumbnails.of(inputStream).scale(1f).outputQuality(quality).toOutputStream(outputStream); + + // 获取文件扩展名和contentType + String extension = originalFilename.substring(originalFilename.lastIndexOf('.') + 1).toLowerCase(); + String contentType = getContentType(originalFilename); + + // 创建MultipartFile对象 + byte[] compressedBytes = outputStream.toByteArray(); + ByteArrayInputStream input = new ByteArrayInputStream(compressedBytes); + return new MockMultipartFile("file", originalFilename, contentType, input); + } finally { + // 确保流关闭 + outputStream.close(); + inputStream.close(); + } + } + +} \ No newline at end of file diff --git a/mes-common/src/main/java/top/mes/admin/common/util/SecureUtils.java b/mes-common/src/main/java/top/mes/admin/common/util/SecureUtils.java new file mode 100644 index 0000000..9191ba8 --- /dev/null +++ b/mes-common/src/main/java/top/mes/admin/common/util/SecureUtils.java @@ -0,0 +1,91 @@ +package top.wms.admin.common.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.extra.spring.SpringUtil; +import top.wms.admin.common.config.properties.RsaProperties; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.security.crypto.autoconfigure.CryptoProperties; +import top.continew.starter.security.crypto.encryptor.AesEncryptor; +import top.continew.starter.security.crypto.encryptor.IEncryptor; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 加密/解密工具类 + * + * @author Charles7c + * @since 2022/12/21 21:41 + */ +public class SecureUtils { + + private SecureUtils() { + } + + /** + * 公钥加密 + * + * @param data 要加密的内容 + * @return 加密后的内容 + */ + public static String encryptByRsaPublicKey(String data) { + String publicKey = RsaProperties.PUBLIC_KEY; + ValidationUtils.throwIfBlank(publicKey, "请配置 RSA 公钥"); + return encryptByRsaPublicKey(data, publicKey); + } + + /** + * 私钥解密 + * + * @param data 要解密的内容(Base64 加密过) + * @return 解密后的内容 + */ + public static String decryptByRsaPrivateKey(String data) { + String privateKey = RsaProperties.PRIVATE_KEY; + ValidationUtils.throwIfBlank(privateKey, "请配置 RSA 私钥"); + return decryptByRsaPrivateKey(data, privateKey); + } + + /** + * 公钥加密 + * + * @param data 要加密的内容 + * @param publicKey 公钥 + * @return 加密后的内容 + */ + public static String encryptByRsaPublicKey(String data, String publicKey) { + return new String(SecureUtil.rsa(null, publicKey).encrypt(data, KeyType.PublicKey)); + } + + /** + * 私钥解密 + * + * @param data 要解密的内容(Base64 加密过) + * @param privateKey 私钥 + * @return 解密后的内容 + */ + public static String decryptByRsaPrivateKey(String data, String privateKey) { + return new String(SecureUtil.rsa(privateKey, null).decrypt(Base64.decode(data), KeyType.PrivateKey)); + } + + /** + * 对普通加密字段列表进行AES加密,优化starter加密模块后优化这个方法 + * + * @param values 待加密内容 + * @return 加密后内容 + */ + public static List encryptFieldByAes(List values) { + IEncryptor encryptor = new AesEncryptor(); + CryptoProperties properties = SpringUtil.getBean(CryptoProperties.class); + return values.stream().map(value -> { + try { + return encryptor.encrypt(value, properties.getPassword(), properties.getPublicKey()); + } catch (Exception e) { + throw new BusinessException("字段加密异常"); + } + }).collect(Collectors.toList()); + } +} diff --git a/mes-extension/mes-extension-schedule-server/pom.xml b/mes-extension/mes-extension-schedule-server/pom.xml new file mode 100644 index 0000000..71c96b3 --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + top.wms + wms-extension + ${revision} + + + wms-extension-schedule-server + 任务调度服务端 + + + + 1.2.0 + + + + + + com.aizuda + snail-job-server-starter + ${snail-job.version} + + + + + org.liquibase + liquibase-core + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/mes-extension/mes-extension-schedule-server/src/main/java/top/mes/admin/extension/scheduling/ScheduleServerApplication.java b/mes-extension/mes-extension-schedule-server/src/main/java/top/mes/admin/extension/scheduling/ScheduleServerApplication.java new file mode 100644 index 0000000..72e3f1d --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/src/main/java/top/mes/admin/extension/scheduling/ScheduleServerApplication.java @@ -0,0 +1,43 @@ +package top.wms.admin.extension.scheduling; + +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.spring.SpringUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.web.ServerProperties; + +/** + * 任务调度服务端启动程序 + * + * @author KAI + * @since 2024/6/25 22:24 + */ +@Slf4j +@SpringBootApplication +@RequiredArgsConstructor +public class ScheduleServerApplication extends com.aizuda.snailjob.server.SnailJobServerApplication implements ApplicationRunner { + + private final ServerProperties serverProperties; + + public static void main(String[] args) { + SpringApplication.run(ScheduleServerApplication.class, args); + } + + @Override + public void run(ApplicationArguments args) { + String hostAddress = NetUtil.getLocalhostStr(); + Integer port = serverProperties.getPort(); + String contextPath = serverProperties.getServlet().getContextPath(); + String baseUrl = URLUtil.normalize("%s:%s%s".formatted(hostAddress, port, contextPath)); + log.info("----------------------------------------------"); + log.info("{} service started successfully.", SpringUtil.getApplicationName()); + log.info("访问地址:{}", baseUrl); + log.info("在线文档:https://snailjob.opensnail.com"); + log.info("----------------------------------------------"); + } +} diff --git a/mes-extension/mes-extension-schedule-server/src/main/resources/config/application-dev.yml b/mes-extension/mes-extension-schedule-server/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..5e62648 --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/src/main/resources/config/application-dev.yml @@ -0,0 +1,57 @@ +server: + port: 9203 + +--- ### 数据源配置 +spring.datasource: +# url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:continew_job}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&autoReconnect=true&maxReconnects=10&failOverReadOnly=false +# username: ${DB_USER:root} +# password: ${DB_PWD:root} + + url: jdbc:mysql://${DB_HOST:192.168.2.30}:${DB_PORT:3306}/${DB_NAME:continew_job}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&autoReconnect=true&maxReconnects=10&failOverReadOnly=false + username: ${DB_USER:root} + password: ${DB_PWD:SQLsql123} + driver-class-name: com.mysql.cj.jdbc.Driver +## Liquibase 配置 +spring.liquibase: + # 是否启用 + enabled: true + # 配置文件路径 + change-log: classpath:/db/changelog/db.changelog-master.yaml + +--- ### Snail Job 服务端配置 +snail-job: + # Netty 端口 + netty-port: 1788 + # 合并日志默认保存天数 + merge-Log-days: 1 + # 合并日志默认的条数 + merge-Log-num: 500 + # 配置日志保存时间(单位:天) + log-storage: 90 + # 配置每批次拉取重试数据的大小 + retry-pull-page-size: 100 + # 配置一个客户端每秒最多接收的重试数量指令 + limiter: 10 + # 配置号段模式下的步长 + step: 100 + # bucket 的总数量 + bucket-total: 128 + # Dashboard 任务容错天数 + summary-day: 7 + # 配置负载均衡周期时间 + load-balance-cycle-time: 10 + ## 回调配置 + callback: + # 回调 uniqueId 前缀 + prefix: CB + # 配置回调的最大执行次数 + max-count: 288 + # 配置回调触发的间隔时间 + trigger-interval: 900 + +--- ### 日志配置 +logging: + level: + com.aizuda.snailjob: DEBUG + file: + path: ./logs \ No newline at end of file diff --git a/mes-extension/mes-extension-schedule-server/src/main/resources/config/application-prod.yml b/mes-extension/mes-extension-schedule-server/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..372f0a5 --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/src/main/resources/config/application-prod.yml @@ -0,0 +1,58 @@ +server: + port: 18001 + +--- ### 数据源配置 +spring.datasource: + url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:wms_admin_job}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&autoReconnect=true&maxReconnects=10&failOverReadOnly=false + username: ${DB_USER:root} + password: ${DB_PWD:123456} + driver-class-name: com.mysql.cj.jdbc.Driver +# # PostgreSQL 配置 +# url: jdbc:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:wms_admin_job}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&autoReconnect=true&maxReconnects=10&failOverReadOnly=false +# username: ${DB_USER:root} +# password: ${DB_PWD:123456} +# driver-class-name: org.postgresql.Driver +## Liquibase 配置 +spring.liquibase: + # 是否启用 + enabled: true + # 配置文件路径 + change-log: classpath:/db/changelog/db.changelog-master.yaml + +--- ### Snail Job 服务端配置 +snail-job: + # Netty 端口 + netty-port: 1788 + # 合并日志默认保存天数 + merge-Log-days: 1 + # 合并日志默认的条数 + merge-Log-num: 500 + # 配置日志保存时间(单位:天) + log-storage: 90 + # 配置每批次拉取重试数据的大小 + retry-pull-page-size: 100 + # 配置一个客户端每秒最多接收的重试数量指令 + limiter: 10 + # 配置号段模式下的步长 + step: 100 + # bucket 的总数量 + bucket-total: 128 + # Dashboard 任务容错天数 + summary-day: 7 + # 配置负载均衡周期时间 + load-balance-cycle-time: 10 + ## 回调配置 + callback: + # 回调 uniqueId 前缀 + prefix: CB + # 配置回调的最大执行次数 + max-count: 288 + # 配置回调触发的间隔时间 + trigger-interval: 900 + +--- ### 日志配置 +logging: + level: + com.aizuda.snailjob: INFO + file: + path: ../logs \ No newline at end of file diff --git a/mes-extension/mes-extension-schedule-server/src/main/resources/config/application.yml b/mes-extension/mes-extension-schedule-server/src/main/resources/config/application.yml new file mode 100644 index 0000000..2adc080 --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/src/main/resources/config/application.yml @@ -0,0 +1,12 @@ +--- ### Spring 配置 +spring: + application: + name: wms-admin-schedule-server + ## 环境配置 + profiles: + # 启用的环境 + active: dev + +--- ### 日志配置 +logging: + config: classpath:logback-spring.xml \ No newline at end of file diff --git a/mes-extension/mes-extension-schedule-server/src/main/resources/db/changelog/db.changelog-master.yaml b/mes-extension/mes-extension-schedule-server/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..286c9bb --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,5 @@ +databaseChangeLog: + - include: + file: db/changelog/mysql/snail-job_table.sql + - include: + file: db/changelog/mysql/snail-job_data.sql \ No newline at end of file diff --git a/mes-extension/mes-extension-schedule-server/src/main/resources/logback-spring.xml b/mes-extension/mes-extension-schedule-server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..8a911d2 --- /dev/null +++ b/mes-extension/mes-extension-schedule-server/src/main/resources/logback-spring.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + ${LOG_CHARSET} + + + + + + + ${FILE_LOG_PATTERN} + ${LOG_CHARSET} + + + + + + + ${LOG_PATH}/${APP_NAME}.log + + + + ${LOG_PATH}/%d{yyyy-MM-dd}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz + + ${FILE_MAX_SIZE} + + ${FILE_MAX_HISTORY} + + + ${FILE_LOG_PATTERN} + ${LOG_CHARSET} + + + + + + + 0 + + 512 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mes-extension/pom.xml b/mes-extension/pom.xml new file mode 100644 index 0000000..38447ca --- /dev/null +++ b/mes-extension/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + top.wms + wms-admin + ${revision} + + + wms-extension + pom + 扩展模块(存放其他扩展模块) + + + wms-extension-schedule-server + + diff --git a/mes-module-system/pom.xml b/mes-module-system/pom.xml new file mode 100644 index 0000000..2e58377 --- /dev/null +++ b/mes-module-system/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + top.wms + wms-admin + ${revision} + + + wms-module-system + 系统管理模块(存放系统管理相关业务功能,例如:部门管理、角色管理、用户管理等) + + + + + top.wms + wms-common + + + + javax.annotation + javax.annotation-api + 1.3.2 + provided + + + + + com.fazecast + jSerialComm + 2.10.5 + + + diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/AbstractLoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/AbstractLoginHandler.java new file mode 100644 index 0000000..c708a85 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/AbstractLoginHandler.java @@ -0,0 +1,111 @@ +package top.wms.admin.auth; + +import cn.dev33.satoken.stp.SaLoginModel; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import top.wms.admin.auth.model.req.LoginReq; +import top.wms.admin.common.context.RoleContext; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.context.UserExtraContext; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.resp.ClientResp; +import top.wms.admin.system.service.DeptService; +import top.wms.admin.system.service.OptionService; +import top.wms.admin.system.service.RoleService; +import top.wms.admin.system.service.UserService; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.core.validation.Validator; +import top.continew.starter.web.util.SpringWebUtils; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import static top.wms.admin.system.enums.PasswordPolicyEnum.PASSWORD_EXPIRATION_DAYS; + +/** + * 登录处理器基类 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 14:52 + */ +@Component +public abstract class AbstractLoginHandler implements LoginHandler { + + @Resource + protected OptionService optionService; + @Resource + protected UserService userService; + @Resource + protected RoleService roleService; + @Resource + private DeptService deptService; + @Resource + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + protected static final String CAPTCHA_EXPIRED = "验证码已失效"; + protected static final String CAPTCHA_ERROR = "验证码错误"; + protected static final String CLIENT_ID = "clientId"; + + @Override + public void preLogin(T req, ClientResp client, HttpServletRequest request) { + // 参数校验 + Validator.validate(req); + } + + @Override + public void postLogin(T req, ClientResp client, HttpServletRequest request) { + } + + /** + * 认证 + * + * @param user 用户信息 + * @param client 终端信息 + * @return token 令牌信息 + */ + protected String authenticate(UserDO user, ClientResp client) { + // 获取权限、角色、密码过期天数 + Long userId = user.getId(); + CompletableFuture> permissionFuture = CompletableFuture.supplyAsync(() -> roleService + .listPermissionByUserId(userId), threadPoolTaskExecutor); + CompletableFuture> roleFuture = CompletableFuture.supplyAsync(() -> roleService + .listByUserId(userId), threadPoolTaskExecutor); + CompletableFuture passwordExpirationDaysFuture = CompletableFuture.supplyAsync(() -> optionService + .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name())); + CompletableFuture.allOf(permissionFuture, roleFuture, passwordExpirationDaysFuture); + UserContext userContext = new UserContext(permissionFuture.join(), roleFuture + .join(), passwordExpirationDaysFuture.join()); + BeanUtil.copyProperties(user, userContext); + // 设置登录配置参数 + SaLoginModel model = new SaLoginModel(); + model.setActiveTimeout(client.getActiveTimeout()); + model.setTimeout(client.getTimeout()); + model.setDevice(client.getClientType()); + userContext.setClientType(client.getClientType()); + model.setExtra(CLIENT_ID, client.getClientId()); + userContext.setClientId(client.getClientId()); + // 登录并缓存用户信息 + StpUtil.login(userContext.getId(), model.setExtraData(BeanUtil.beanToMap(new UserExtraContext(SpringWebUtils + .getRequest())))); + UserContextHolder.setContext(userContext); + return StpUtil.getTokenValue(); + } + + /** + * 检查用户状态 + * + * @param user 用户信息 + */ + protected void checkUserStatus(UserDO user) { + CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, user.getStatus(), "此账号已被禁用,如有疑问,请联系管理员"); + // DeptDO dept = deptService.getById(user.getDeptId()); + // CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员"); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/LoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/LoginHandler.java new file mode 100644 index 0000000..f4c0c42 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/LoginHandler.java @@ -0,0 +1,52 @@ +package top.wms.admin.auth; + +import jakarta.servlet.http.HttpServletRequest; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.LoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.system.model.resp.ClientResp; + +/** + * 登录处理器 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 14:52 + */ +public interface LoginHandler { + + /** + * 登录 + * + * @param req 登录请求参数 + * @param client 终端信息 + * @param request 请求对象 + * @return 登录响应参数 + */ + LoginResp login(T req, ClientResp client, HttpServletRequest request); + + /** + * 登录前置处理 + * + * @param req 登录请求参数 + * @param client 终端信息 + * @param request 请求对象 + */ + void preLogin(T req, ClientResp client, HttpServletRequest request); + + /** + * 登录后置处理 + * + * @param req 登录请求参数 + * @param client 终端信息 + * @param request 请求对象 + */ + void postLogin(T req, ClientResp client, HttpServletRequest request); + + /** + * 获取认证类型 + * + * @return 认证类型 + */ + AuthTypeEnum getAuthType(); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/LoginHandlerFactory.java b/mes-module-system/src/main/java/top/mes/admin/auth/LoginHandlerFactory.java new file mode 100644 index 0000000..5a082da --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/LoginHandlerFactory.java @@ -0,0 +1,40 @@ +package top.wms.admin.auth; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.LoginReq; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * 登录处理器工厂 + * + * @author KAI + * @author Charles7c + * @since 2024/12/20 15:16 + */ +@Component +public class LoginHandlerFactory { + + private final Map> handlerMap = new EnumMap<>(AuthTypeEnum.class); + + @Autowired + public LoginHandlerFactory(List> handlers) { + for (LoginHandler handler : handlers) { + handlerMap.put(handler.getAuthType(), handler); + } + } + + /** + * 根据认证类型获取 + * + * @param authType 认证类型 + * @return 认证处理器 + */ + public LoginHandler getHandler(AuthTypeEnum authType) { + return (LoginHandler)handlerMap.get(authType); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/enums/AuthTypeEnum.java b/mes-module-system/src/main/java/top/mes/admin/auth/enums/AuthTypeEnum.java new file mode 100644 index 0000000..c2cdf0d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/enums/AuthTypeEnum.java @@ -0,0 +1,47 @@ +package top.wms.admin.auth.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 认证类型枚举 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 14:52 + */ +@Getter +@RequiredArgsConstructor +public enum AuthTypeEnum implements BaseEnum { + + /** + * 账号 + */ + ACCOUNT("ACCOUNT", "账号", UiConstants.COLOR_SUCCESS), + + /** + * 邮箱 + */ + EMAIL("EMAIL", "邮箱", UiConstants.COLOR_PRIMARY), + + /** + * 手机号 + */ + PHONE("PHONE", "手机号", UiConstants.COLOR_PRIMARY), + + /** + * 第三方账号 + */ + SOCIAL("SOCIAL", "第三方账号", UiConstants.COLOR_ERROR), + + /** + * 卡号 + */ + CARD("CARD", "卡号", UiConstants.COLOR_PRIMARY); + + private final String value; + private final String description; + private final String color; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/handler/AccountLoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/handler/AccountLoginHandler.java new file mode 100644 index 0000000..6913b2c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/handler/AccountLoginHandler.java @@ -0,0 +1,110 @@ +package top.wms.admin.auth.handler; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import top.wms.admin.auth.AbstractLoginHandler; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.AccountLoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.common.util.SecureUtils; +import top.wms.admin.system.enums.PasswordPolicyEnum; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.resp.ClientResp; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.util.ExceptionUtils; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.core.validation.ValidationUtils; + +import java.time.Duration; + +/** + * 账号登录处理器 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 14:58 + */ +@Component +@RequiredArgsConstructor +public class AccountLoginHandler extends AbstractLoginHandler { + + private final PasswordEncoder passwordEncoder; + + @Override + public LoginResp login(AccountLoginReq req, ClientResp client, HttpServletRequest request) { + // 解密密码 + String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); + ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); + // 验证用户名密码 + String username = req.getUsername(); + UserDO user = userService.getByUsername(username); + boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(rawPassword, user.getPassword()); + // 检查账号锁定状态 + this.checkUserLocked(req.getUsername(), request, isError); + ValidationUtils.throwIf(isError, "用户名或密码错误"); + // 检查用户状态 + super.checkUserStatus(user); + // 执行认证 + String token = this.authenticate(user, client); + return LoginResp.builder().token(token).build(); + } + + @Override + public void preLogin(AccountLoginReq req, ClientResp client, HttpServletRequest request) { + super.preLogin(req, client, request); + // 校验验证码 + int loginCaptchaEnabled = optionService.getValueByCode2Int("LOGIN_CAPTCHA_ENABLED"); + if (SysConstants.YES.equals(loginCaptchaEnabled)) { + ValidationUtils.throwIfBlank(req.getCaptcha(), "验证码不能为空"); + ValidationUtils.throwIfBlank(req.getUuid(), "验证码标识不能为空"); + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + req.getUuid(); + String captcha = RedisUtils.get(captchaKey); + ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); + RedisUtils.delete(captchaKey); + ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR); + } + } + + @Override + public AuthTypeEnum getAuthType() { + return AuthTypeEnum.ACCOUNT; + } + + /** + * 检测用户是否已被锁定 + * + * @param username 用户名 + * @param request 请求对象 + * @param isError 是否登录错误 + */ + private void checkUserLocked(String username, HttpServletRequest request, boolean isError) { + // 不锁定 + int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name()); + if (maxErrorCount <= SysConstants.NO) { + return; + } + // 检测是否已被锁定 + String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil + .getClientIP(request)); + int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name()); + Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0); + CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.getMsg() + .formatted(lockMinutes)); + // 登录成功清除计数 + if (!isError) { + RedisUtils.delete(key); + return; + } + // 登录失败递增计数 + currentErrorCount++; + RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); + CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.getMsg() + .formatted(maxErrorCount, lockMinutes)); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/handler/CardLoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/handler/CardLoginHandler.java new file mode 100644 index 0000000..745fa3a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/handler/CardLoginHandler.java @@ -0,0 +1,37 @@ +package top.wms.admin.auth.handler; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import top.continew.starter.core.validation.ValidationUtils; +import top.wms.admin.auth.AbstractLoginHandler; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.CardLoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.resp.ClientResp; + +@Component +public class CardLoginHandler extends AbstractLoginHandler { + + @Override + public LoginResp login(CardLoginReq req, ClientResp client, HttpServletRequest request) { + // 验证手机号 + UserDO user = userService.getByCard(req.getCardNumber()); + ValidationUtils.throwIfNull(user, "此卡号未绑定本系统账号"); + // 检查用户状态 + super.checkUserStatus(user); + // 执行认证 + String token = super.authenticate(user, client); + return LoginResp.builder().token(token).build(); + } + + @Override + public void preLogin(CardLoginReq req, ClientResp client, HttpServletRequest request) { + super.preLogin(req, client, request); + } + + @Override + public AuthTypeEnum getAuthType() { + return AuthTypeEnum.CARD; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/handler/EmailLoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/handler/EmailLoginHandler.java new file mode 100644 index 0000000..c5aae56 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/handler/EmailLoginHandler.java @@ -0,0 +1,51 @@ +package top.wms.admin.auth.handler; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import top.wms.admin.auth.AbstractLoginHandler; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.EmailLoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.resp.ClientResp; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.validation.ValidationUtils; + +/** + * 邮箱登录处理器 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 14:58 + */ +@Component +public class EmailLoginHandler extends AbstractLoginHandler { + + @Override + public LoginResp login(EmailLoginReq req, ClientResp client, HttpServletRequest request) { + // 验证邮箱 + UserDO user = userService.getByEmail(req.getEmail()); + ValidationUtils.throwIfNull(user, "此邮箱未绑定本系统账号"); + // 检查用户状态 + super.checkUserStatus(user); + // 执行认证 + String token = super.authenticate(user, client); + return LoginResp.builder().token(token).build(); + } + + @Override + public void preLogin(EmailLoginReq req, ClientResp client, HttpServletRequest request) { + String email = req.getEmail(); + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; + String captcha = RedisUtils.get(captchaKey); + ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); + ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR); + RedisUtils.delete(captchaKey); + } + + @Override + public AuthTypeEnum getAuthType() { + return AuthTypeEnum.EMAIL; + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/handler/PhoneLoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/handler/PhoneLoginHandler.java new file mode 100644 index 0000000..e40b5ed --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/handler/PhoneLoginHandler.java @@ -0,0 +1,51 @@ +package top.wms.admin.auth.handler; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import top.wms.admin.auth.AbstractLoginHandler; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.PhoneLoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.resp.ClientResp; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.validation.ValidationUtils; + +/** + * 手机号登录处理器 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 14:59 + */ +@Component +public class PhoneLoginHandler extends AbstractLoginHandler { + + @Override + public LoginResp login(PhoneLoginReq req, ClientResp client, HttpServletRequest request) { + // 验证手机号 + UserDO user = userService.getByPhone(req.getPhone()); + ValidationUtils.throwIfNull(user, "此手机号未绑定本系统账号"); + // 检查用户状态 + super.checkUserStatus(user); + // 执行认证 + String token = super.authenticate(user, client); + return LoginResp.builder().token(token).build(); + } + + @Override + public void preLogin(PhoneLoginReq req, ClientResp client, HttpServletRequest request) { + String phone = req.getPhone(); + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone; + String captcha = RedisUtils.get(captchaKey); + ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); + ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR); + RedisUtils.delete(captchaKey); + } + + @Override + public AuthTypeEnum getAuthType() { + return AuthTypeEnum.PHONE; + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/handler/SocialLoginHandler.java b/mes-module-system/src/main/java/top/mes/admin/auth/handler/SocialLoginHandler.java new file mode 100644 index 0000000..1b49cbf --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/handler/SocialLoginHandler.java @@ -0,0 +1,44 @@ +package top.wms.admin.auth.handler; + +import cn.dev33.satoken.stp.StpUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import top.wms.admin.auth.AbstractLoginHandler; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.SocialLoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.system.model.resp.ClientResp; +import top.continew.starter.core.exception.BadRequestException; + +/** + * 第三方账号登录处理器 + * + * @author KAI + * @author Charles7c + * @since 2024/12/25 14:21 + */ +@Component +@RequiredArgsConstructor +public class SocialLoginHandler extends AbstractLoginHandler { + + @Override + public LoginResp login(SocialLoginReq req, ClientResp client, HttpServletRequest request) { + // 第三方登录已禁用 + throw new BadRequestException("第三方登录功能已禁用"); + } + + @Override + public void preLogin(SocialLoginReq req, ClientResp client, HttpServletRequest request) { + super.preLogin(req, client, request); + if (StpUtil.isLogin()) { + StpUtil.logout(); + } + } + + @Override + public AuthTypeEnum getAuthType() { + return AuthTypeEnum.SOCIAL; + } + +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/query/OnlineUserQuery.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/query/OnlineUserQuery.java new file mode 100644 index 0000000..28db913 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/query/OnlineUserQuery.java @@ -0,0 +1,56 @@ +package top.wms.admin.auth.model.query; + +import cn.hutool.core.date.DatePattern; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 在线用户查询条件 + * + * @author Charles7c + * @since 2023/1/20 23:07 + */ +@Data +@Schema(description = "在线用户查询条件") +public class OnlineUserQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户昵称 + */ + @Schema(description = "用户昵称", example = "张三") + private String nickname; + + /** + * 终端 ID + */ + @Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a") + private String clientId; + + /** + * 登录时间 + */ + @Schema(description = "登录时间", example = "2023-08-08 00:00:00,2023-08-08 23:59:59") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private List loginTime; + + /** + * 用户 ID + */ + @Schema(hidden = true) + private Long userId; + + /** + * 角色 ID + */ + @Schema(hidden = true) + private Long roleId; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/req/AccountLoginReq.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/AccountLoginReq.java new file mode 100644 index 0000000..0d60b8c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/AccountLoginReq.java @@ -0,0 +1,47 @@ +package top.wms.admin.auth.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; + +/** + * 账号登录参数 + * + * @author Charles7c + * @since 2022/12/21 20:43 + */ +@Data +@Schema(description = "账号登录参数") +public class AccountLoginReq extends LoginReq { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + @NotBlank(message = "用户名不能为空") + private String username; + + /** + * 密码(加密) + */ + @Schema(description = "密码(加密)", example = "HHwZoiBwCfh0xLdWOAd0bHOkEZlIMMOQKJyeFUw9T3ArrhL57od2i42s1o0sSXKkeHPJXvQsninhPFH2lArDDQ==") + @NotBlank(message = "密码不能为空") + private String password; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "ABCD") + private String captcha; + + /** + * 验证码标识 + */ + @Schema(description = "验证码标识", example = "090b9a2c-1691-4fca-99db-e4ed0cff362f") + private String uuid; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/req/CardLoginReq.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/CardLoginReq.java new file mode 100644 index 0000000..6e1dd9c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/CardLoginReq.java @@ -0,0 +1,30 @@ +package top.wms.admin.auth.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; + +/** + * 卡号登录参数 + * + * @author KAI + * @author Charles7c + * @since 2024/12/25 15:43 + */ +@Data +@Schema(description = "卡号登录参数") +public class CardLoginReq extends LoginReq { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 卡号 + */ + @Schema(description = "卡号", example = "1234567890") + @NotBlank(message = "卡号不能为空") + private String cardNumber; + +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/req/EmailLoginReq.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/EmailLoginReq.java new file mode 100644 index 0000000..3c42481 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/EmailLoginReq.java @@ -0,0 +1,40 @@ +package top.wms.admin.auth.model.req; + +import cn.hutool.core.lang.RegexPool; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; + +/** + * 邮箱登录参数 + * + * @author Charles7c + * @since 2023/10/23 20:15 + */ +@Data +@Schema(description = "邮箱登录参数") +public class EmailLoginReq extends LoginReq { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 邮箱 + */ + @Schema(description = "邮箱", example = "123456789@qq.com") + @NotBlank(message = "邮箱不能为空") + @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") + private String email; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "888888") + @NotBlank(message = "验证码不能为空") + @Length(max = 6, message = "验证码非法") + private String captcha; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/req/LoginReq.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/LoginReq.java new file mode 100644 index 0000000..1e2c84c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/LoginReq.java @@ -0,0 +1,47 @@ +package top.wms.admin.auth.model.req; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import top.wms.admin.auth.enums.AuthTypeEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 登录参数基类 + * + * @author KAI + * @author Charles7c + * @since 2024/12/22 15:16 + */ +@Data +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "authType", visible = true) +@JsonSubTypes({@JsonSubTypes.Type(value = AccountLoginReq.class, name = "ACCOUNT"), + @JsonSubTypes.Type(value = EmailLoginReq.class, name = "EMAIL"), + @JsonSubTypes.Type(value = PhoneLoginReq.class, name = "PHONE"), + @JsonSubTypes.Type(value = SocialLoginReq.class, name = "SOCIAL"), + @JsonSubTypes.Type(value = CardLoginReq.class, name = "CARD")}) +@Schema(description = "基础登录参数") +public class LoginReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 终端 ID + */ + @Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a") + @NotBlank(message = "终端ID不能为空") + private String clientId; + + /** + * 认证类型 + */ + @Schema(description = "认证类型", example = "ACCOUNT") + @NotNull(message = "认证类型非法") + private AuthTypeEnum authType; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/req/PhoneLoginReq.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/PhoneLoginReq.java new file mode 100644 index 0000000..b38c901 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/PhoneLoginReq.java @@ -0,0 +1,40 @@ +package top.wms.admin.auth.model.req; + +import cn.hutool.core.lang.RegexPool; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; + +/** + * 手机号登录参数 + * + * @author Charles7c + * @since 2023/10/26 22:37 + */ +@Data +@Schema(description = "手机号登录参数") +public class PhoneLoginReq extends LoginReq { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 手机号 + */ + @Schema(description = "手机号", example = "13811111111") + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = RegexPool.MOBILE, message = "手机号格式错误") + private String phone; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "8888") + @NotBlank(message = "验证码不能为空") + @Length(max = 4, message = "验证码非法") + private String captcha; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/req/SocialLoginReq.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/SocialLoginReq.java new file mode 100644 index 0000000..f938ccb --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/req/SocialLoginReq.java @@ -0,0 +1,43 @@ +package top.wms.admin.auth.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; + +/** + * 第三方账号登录参数 + * + * @author KAI + * @author Charles7c + * @since 2024/12/25 15:43 + */ +@Data +@Schema(description = "第三方账号登录参数") +public class SocialLoginReq extends LoginReq { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 第三方登录平台 + */ + @Schema(description = "第三方登录平台", example = "gitee") + @NotBlank(message = "第三方登录平台不能为空") + private String source; + + /** + * 授权码 + */ + @Schema(description = "授权码", example = "a08d33e9e577fb339de027499784ed4e871d6f62ae65b459153e906ab546bd56") + @NotBlank(message = "授权码不能为空") + private String code; + + /** + * 状态码 + */ + @Schema(description = "状态码", example = "2ca8d8baf437eb374efaa1191a3d") + @NotBlank(message = "状态码不能为空") + private String state; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/CaptchaResp.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/CaptchaResp.java new file mode 100644 index 0000000..af2dca9 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/CaptchaResp.java @@ -0,0 +1,59 @@ +package top.wms.admin.auth.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 验证码信息 + * + * @author Charles7c + * @since 2022/12/11 13:55 + */ +@Data +@Builder +@Schema(description = "验证码信息") +public class CaptchaResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 验证码标识 + */ + @Schema(description = "验证码标识", example = "090b9a2c-1691-4fca-99db-e4ed0cff362f") + private String uuid; + + /** + * 验证码图片(Base64编码,带图片格式:data:image/gif;base64) + */ + @Schema(description = "验证码图片(Base64编码,带图片格式:data:image/gif;base64)", example = "data:image/png;base64,iVBORw0KGgoAAAAN...") + private String img; + + /** + * 过期时间戳 + */ + @Schema(description = "过期时间戳", example = "1714376969409") + private Long expireTime; + + /** + * 是否启用 + */ + @Schema(description = "是否启用", example = "true") + private Boolean isEnabled; + + /** + * 构建验证码信息 + * + * @param uuid 验证码标识 + * @param img 验证码图片(Base64编码,带图片格式:data:image/gif;base64) + * @param expireTime 过期时间戳 + * @return 验证码信息 + */ + public static CaptchaResp of(String uuid, String img, Long expireTime) { + return CaptchaResp.builder().uuid(uuid).img(img).expireTime(expireTime).isEnabled(true).build(); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/LoginResp.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/LoginResp.java new file mode 100644 index 0000000..8ff5f37 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/LoginResp.java @@ -0,0 +1,29 @@ +package top.wms.admin.auth.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 登录响应参数 + * + * @author Charles7c + * @since 2022/12/21 20:42 + */ +@Data +@Builder +@Schema(description = "登录响应参数") +public class LoginResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 令牌 + */ + @Schema(description = "令牌", example = "eyJ0eXAiOiJlV1QiLCJhbGciqiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb29pbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiSjd4SUljYnU5cmNwU09vQ3Uyc1ND1BYYTYycFRjcjAifQ.KUPOYm-2wfuLUSfEEAbpGE527fzmkAJG7sMNcQ0pUZ8") + private String token; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/OnlineUserResp.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/OnlineUserResp.java new file mode 100644 index 0000000..d61dd38 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/OnlineUserResp.java @@ -0,0 +1,102 @@ +package top.wms.admin.auth.model.resp; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.annotation.AssembleMethod; +import cn.crane4j.annotation.ContainerMethod; +import cn.crane4j.annotation.MappingType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.auth.service.OnlineUserService; +import top.wms.admin.common.constant.ContainerConstants; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 在线用户信息 + * + * @author Charles7c + * @since 2023/1/20 21:54 + */ +@Data +@Schema(description = "在线用户信息") +public class OnlineUserResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + @Assemble(prop = ":nickname", container = ContainerConstants.USER_NICKNAME) + private Long id; + + /** + * 令牌 + */ + @Schema(description = "令牌", example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiTUd6djdyOVFoeHEwdVFqdFAzV3M5YjVJRzh4YjZPSEUifQ.7q7U3ouoN7WPhH2kUEM7vPe5KF3G_qavSG-vRgIxKvE") + @AssembleMethod(prop = ":lastActiveTime", targetType = OnlineUserService.class, method = @ContainerMethod(bindMethod = "getLastActiveTime", type = MappingType.ORDER_OF_KEYS)) + private String token; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + private String username; + + /** + * 昵称 + */ + @Schema(description = "昵称", example = "张三") + private String nickname; + + /** + * 终端类型 + */ + @Schema(description = "终端类型", example = "PC") + private String clientType; + + /** + * 终端 ID + */ + @Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a") + private String clientId; + + /** + * 登录 IP + */ + @Schema(description = "登录 IP", example = "") + private String ip; + + /** + * 登录地点 + */ + @Schema(description = "登录地点", example = "中国北京北京市") + private String address; + + /** + * 浏览器 + */ + @Schema(description = "浏览器", example = "Chrome 115.0.0.0") + private String browser; + + /** + * 操作系统 + */ + @Schema(description = "操作系统", example = "Windows 10") + private String os; + + /** + * 登录时间 + */ + @Schema(description = "登录时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime loginTime; + + /** + * 最后活跃时间 + */ + @Schema(description = "最后活跃时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime lastActiveTime; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/RouteResp.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/RouteResp.java new file mode 100644 index 0000000..a5e9d10 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/RouteResp.java @@ -0,0 +1,114 @@ +package top.wms.admin.auth.model.resp; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 路由信息 + * + * @author Charles7c + * @since 2023/2/26 22:51 + */ +@Data +@Schema(description = "路由信息") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouteResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1010") + private Long id; + + /** + * 上级菜单 ID + */ + @Schema(description = "上级菜单ID", example = "1000") + private Long parentId; + + /** + * 标题 + */ + @Schema(description = "标题", example = "用户管理") + private String title; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + private Integer type; + + /** + * 路由地址 + */ + @Schema(description = "路由地址", example = "/system/user") + private String path; + + /** + * 组件名称 + */ + @Schema(description = "组件名称", example = "User") + private String name; + + /** + * 组件路径 + */ + @Schema(description = "组件路径", example = "/system/user/index") + private String component; + + /** + * 重定向地址 + */ + @Schema(description = "重定向地址") + private String redirect; + + /** + * 图标 + */ + @Schema(description = "图标", example = "user") + private String icon; + + /** + * 是否外链 + */ + @Schema(description = "是否外链", example = "false") + private Boolean isExternal; + + /** + * 是否缓存 + */ + @Schema(description = "是否缓存", example = "false") + private Boolean isCache; + + /** + * 是否隐藏 + */ + @Schema(description = "是否隐藏", example = "false") + private Boolean isHidden; + + /** + * 权限标识 + */ + @Schema(description = "权限标识", example = "system:user:list") + private String permission; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + /** + * 子路由列表 + */ + @Schema(description = "子路由列表") + private List children; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/SocialAuthAuthorizeResp.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/SocialAuthAuthorizeResp.java new file mode 100644 index 0000000..44dab92 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/SocialAuthAuthorizeResp.java @@ -0,0 +1,29 @@ +package top.wms.admin.auth.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 三方账号授权认证响应信息 + * + * @author Charles7c + * @since 2024/3/6 22:26 + */ +@Data +@Builder +@Schema(description = "三方账号授权认证响应信息") +public class SocialAuthAuthorizeResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 授权 URL + */ + @Schema(description = "授权 URL", example = "https://gitee.com/oauth/authorize?response_type=code&client_id=5d271b7f638941812aaf8bfc2e2f08f06d6235ef934e0e39537e2364eb8452c4&redirect_uri=http://localhost:6609/social/callback?source=gitee&state=d4ea7129e2531050210e9c918cc007d7&scope=user_info") + private String authorizeUrl; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/UserInfoResp.java b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/UserInfoResp.java new file mode 100644 index 0000000..66aee66 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/model/resp/UserInfoResp.java @@ -0,0 +1,124 @@ +package top.wms.admin.auth.model.resp; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.enums.GenderEnum; +import top.continew.starter.security.mask.annotation.JsonMask; +import top.continew.starter.security.mask.enums.MaskType; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Set; + +/** + * 用户信息 + * + * @author Charles7c + * @since 2022/12/29 20:15 + */ +@Data +@Schema(description = "用户信息") +public class UserInfoResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + private String username; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1") + private GenderEnum gender; + + /** + * 邮箱 + */ + @Schema(description = "邮箱", example = "c*******@126.com") + @JsonMask(MaskType.EMAIL) + private String email; + + /** + * 手机号码 + */ + @Schema(description = "手机号码", example = "188****8888") + @JsonMask(MaskType.MOBILE_PHONE) + private String phone; + + /** + * 头像地址 + */ + @Schema(description = "头像地址", example = "https://himg.bdimg.com/sys/portrait/item/public.1.81ac9a9e.rf1ix17UfughLQjNo7XQ_w.jpg") + private String avatar; + + /** + * 描述 + */ + @Schema(description = "描述", example = "张三描述信息") + private String description; + + /** + * 最后一次修改密码时间 + */ + @Schema(description = "最后一次修改密码时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime pwdResetTime; + + /** + * 密码是否已过期 + */ + @Schema(description = "密码是否已过期", example = "true") + private Boolean pwdExpired; + + /** + * 创建时间 + */ + @JsonIgnore + private LocalDateTime createTime; + + /** + * 注册日期 + */ + @Schema(description = "注册日期", example = "2023-08-08") + private LocalDate registrationDate; + + /** + * 部门 ID + */ + @Schema(description = "部门 ID", example = "1") + private Long deptId; + + /** + * 所属部门 + */ + @Schema(description = "所属部门", example = "测试部") + private String deptName; + + /** + * 权限码集合 + */ + @Schema(description = "权限码集合", example = "[\"system:user:list\",\"system:user:add\"]") + private Set permissions; + + /** + * 角色编码集合 + */ + @Schema(description = "角色编码集合", example = "[\"test\"]") + private Set roles; + + public LocalDate getRegistrationDate() { + return createTime.toLocalDate(); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/service/AuthService.java b/mes-module-system/src/main/java/top/mes/admin/auth/service/AuthService.java new file mode 100644 index 0000000..8e415be --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/service/AuthService.java @@ -0,0 +1,34 @@ +package top.wms.admin.auth.service; + +import jakarta.servlet.http.HttpServletRequest; +import top.wms.admin.auth.model.req.LoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.auth.model.resp.RouteResp; + +import java.util.List; + +/** + * 认证业务接口 + * + * @author Charles7c + * @since 2022/12/21 21:48 + */ +public interface AuthService { + + /** + * 登录 + * + * @param req 登录请求参数 + * @param request 请求对象 + * @return 登录响应参数 + */ + LoginResp login(LoginReq req, HttpServletRequest request); + + /** + * 构建路由树 + * + * @param userId 用户 ID + * @return 路由树 + */ + List buildRouteTree(Long userId); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/service/OnlineUserService.java b/mes-module-system/src/main/java/top/mes/admin/auth/service/OnlineUserService.java new file mode 100644 index 0000000..3628526 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/service/OnlineUserService.java @@ -0,0 +1,50 @@ +package top.wms.admin.auth.service; + +import top.wms.admin.auth.model.query.OnlineUserQuery; +import top.wms.admin.auth.model.resp.OnlineUserResp; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 在线用户业务接口 + * + * @author Charles7c + * @since 2023/3/25 22:48 + */ +public interface OnlineUserService { + + /** + * 分页查询列表 + * + * @param query 查询条件 + * @param pageQuery 分页查询条件 + * @return 分页列表信息 + */ + PageResp page(OnlineUserQuery query, PageQuery pageQuery); + + /** + * 查询列表 + * + * @param query 查询条件 + * @return 列表信息 + */ + List list(OnlineUserQuery query); + + /** + * 查询 Token 最后活跃时间 + * + * @param token Token + * @return 最后活跃时间 + */ + LocalDateTime getLastActiveTime(String token); + + /** + * 踢出用户 + * + * @param userId 用户 ID + */ + void kickOut(Long userId); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/service/impl/AuthServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/auth/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..7b45cf2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/service/impl/AuthServiceImpl.java @@ -0,0 +1,110 @@ +package top.wms.admin.auth.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.tree.Tree; +import cn.hutool.core.lang.tree.TreeNodeConfig; +import cn.hutool.core.lang.tree.TreeUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.auth.LoginHandler; +import top.wms.admin.auth.LoginHandlerFactory; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.LoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.auth.model.resp.RouteResp; +import top.wms.admin.auth.service.AuthService; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.common.context.RoleContext; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.MenuTypeEnum; +import top.wms.admin.system.model.resp.ClientResp; +import top.wms.admin.system.model.resp.MenuResp; +import top.wms.admin.system.service.ClientService; +import top.wms.admin.system.service.MenuService; +import top.wms.admin.system.service.RoleService; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.extension.crud.annotation.TreeField; +import top.continew.starter.extension.crud.autoconfigure.CrudProperties; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * 认证业务实现 + * + * @author Charles7c + * @since 2022/12/21 21:49 + */ +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + private final LoginHandlerFactory loginHandlerFactory; + private final ClientService clientService; + private final RoleService roleService; + private final MenuService menuService; + private final CrudProperties crudProperties; + + @Override + public LoginResp login(LoginReq req, HttpServletRequest request) { + AuthTypeEnum authType = req.getAuthType(); + // 校验终端 + ClientResp client = clientService.getByClientId(req.getClientId()); + ValidationUtils.throwIfNull(client, "终端不存在"); + ValidationUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(client.getStatus()), "终端已禁用"); + ValidationUtils.throwIf(!client.getAuthType().contains(authType.getValue()), "该终端暂未授权 [{}] 认证", authType + .getDescription()); + // 获取处理器 + LoginHandler loginHandler = loginHandlerFactory.getHandler(authType); + // 登录前置处理 + loginHandler.preLogin(req, client, request); + // 登录 + LoginResp loginResp = loginHandler.login(req, client, request); + // 登录后置处理 + loginHandler.postLogin(req, client, request); + return loginResp; + } + + @Override + public List buildRouteTree(Long userId) { + Set roleSet = roleService.listByUserId(userId); + if (CollUtil.isEmpty(roleSet)) { + return new ArrayList<>(0); + } + // 查询菜单列表 + Set menuSet = new LinkedHashSet<>(); + if (roleSet.stream().anyMatch(r -> SysConstants.SUPER_ROLE_ID.equals(r.getId()))) { + menuSet.addAll(menuService.listByRoleId(SysConstants.SUPER_ROLE_ID)); + } else { + roleSet.forEach(r -> menuSet.addAll(menuService.listByRoleId(r.getId()))); + } + List menuList = menuSet.stream().filter(m -> !MenuTypeEnum.BUTTON.equals(m.getType())).toList(); + if (CollUtil.isEmpty(menuList)) { + return new ArrayList<>(0); + } + // 构建路由树 + TreeField treeField = MenuResp.class.getDeclaredAnnotation(TreeField.class); + TreeNodeConfig treeNodeConfig = crudProperties.getTree().genTreeNodeConfig(treeField); + List> treeList = TreeUtil.build(menuList, treeField.rootId(), treeNodeConfig, (m, tree) -> { + tree.setId(m.getId()); + tree.setParentId(m.getParentId()); + tree.setName(m.getTitle()); + tree.setWeight(m.getSort()); + tree.putExtra("type", m.getType().getValue()); + tree.putExtra("path", m.getPath()); + tree.putExtra("name", m.getName()); + tree.putExtra("component", m.getComponent()); + tree.putExtra("redirect", m.getRedirect()); + tree.putExtra("icon", m.getIcon()); + tree.putExtra("isExternal", m.getIsExternal()); + tree.putExtra("isCache", m.getIsCache()); + tree.putExtra("isHidden", m.getIsHidden()); + tree.putExtra("permission", m.getPermission()); + }); + return BeanUtil.copyToList(treeList, RouteResp.class); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/auth/service/impl/OnlineUserServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/auth/service/impl/OnlineUserServiceImpl.java new file mode 100644 index 0000000..8582154 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/auth/service/impl/OnlineUserServiceImpl.java @@ -0,0 +1,137 @@ +package top.wms.admin.auth.service.impl; + +import cn.crane4j.annotation.AutoOperate; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.auth.model.query.OnlineUserQuery; +import top.wms.admin.auth.model.resp.OnlineUserResp; +import top.wms.admin.auth.service.OnlineUserService; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.context.UserExtraContext; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 在线用户业务实现 + * + * @author Charles7c + * @since 2023/3/25 22:49 + */ +@Service +@RequiredArgsConstructor +public class OnlineUserServiceImpl implements OnlineUserService { + + @Override + @AutoOperate(type = OnlineUserResp.class, on = "list") + public PageResp page(OnlineUserQuery query, PageQuery pageQuery) { + List list = this.list(query); + return PageResp.build(pageQuery.getPage(), pageQuery.getSize(), list); + } + + @Override + public List list(OnlineUserQuery query) { + List list = new ArrayList<>(); + // 查询所有在线 Token + List tokenKeyList = StpUtil.searchTokenValue(StringConstants.EMPTY, 0, -1, false); + Map> tokenMap = tokenKeyList.stream().filter(tokenKey -> { + String token = StrUtil.subAfter(tokenKey, StringConstants.COLON, true); + // 忽略已过期或失效 Token + return StpUtil.getStpLogic().getTokenActiveTimeoutByToken(token) >= SaTokenDao.NEVER_EXPIRE; + }) + .map(tokenKey -> StrUtil.subAfter(tokenKey, StringConstants.COLON, true)) + .collect(Collectors.groupingBy(token -> Convert.toLong(StpUtil.getLoginIdByToken(token)))); + // 筛选数据 + for (Map.Entry> entry : tokenMap.entrySet()) { + Long userId = entry.getKey(); + UserContext userContext = UserContextHolder.getContext(userId); + if (null == userContext || !this.isMatchNickname(query.getNickname(), userContext) || !this + .isMatchClientId(query.getClientId(), userContext)) { + continue; + } + List loginTimeList = query.getLoginTime(); + entry.getValue().parallelStream().forEach(token -> { + UserExtraContext extraContext = UserContextHolder.getExtraContext(token); + if (!this.isMatchLoginTime(loginTimeList, extraContext.getLoginTime())) { + return; + } + OnlineUserResp resp = BeanUtil.copyProperties(userContext, OnlineUserResp.class); + BeanUtil.copyProperties(extraContext, resp); + resp.setToken(token); + list.add(resp); + }); + } + // 设置排序 + CollUtil.sort(list, Comparator.comparing(OnlineUserResp::getLoginTime).reversed()); + return list; + } + + @Override + public LocalDateTime getLastActiveTime(String token) { + long lastActiveTime = StpUtil.getStpLogic().getTokenLastActiveTime(token); + return lastActiveTime == SaTokenDao.NOT_VALUE_EXPIRE ? null : DateUtil.date(lastActiveTime).toLocalDateTime(); + } + + @Override + public void kickOut(Long userId) { + if (!StpUtil.isLogin(userId)) { + return; + } + StpUtil.logout(userId); + } + + /** + * 是否匹配昵称 + * + * @param nickname 昵称 + * @param userContext 用户上下文信息 + * @return 是否匹配昵称 + */ + private boolean isMatchNickname(String nickname, UserContext userContext) { + if (StrUtil.isBlank(nickname)) { + return true; + } + return StrUtil.contains(userContext.getUsername(), nickname) || StrUtil.contains(UserContextHolder + .getNickname(userContext.getId()), nickname); + } + + /** + * 是否匹配终端 ID + * + * @param clientId 终端 ID + * @param userContext 用户上下文信息 + * @return 是否匹配终端 ID + */ + private boolean isMatchClientId(String clientId, UserContext userContext) { + if (StrUtil.isBlank(clientId)) { + return true; + } + return Objects.equals(userContext.getClientId(), clientId); + } + + /** + * 是否匹配登录时间 + * + * @param loginTimeList 登录时间列表 + * @param loginTime 登录时间 + * @return 是否匹配登录时间 + */ + private boolean isMatchLoginTime(List loginTimeList, LocalDateTime loginTime) { + if (CollUtil.isEmpty(loginTimeList)) { + return true; + } + return DateUtil.isIn(DateUtil.date(loginTime).toJdkDate(), loginTimeList.get(0), loginTimeList.get(1)); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileInfoContainer.java b/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileInfoContainer.java new file mode 100644 index 0000000..9810d04 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileInfoContainer.java @@ -0,0 +1,38 @@ +package top.wms.admin.system.config.file; + +import cn.crane4j.core.container.Container; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.system.model.entity.FileDO; +import top.wms.admin.system.service.FileService; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 文件信息填充容器 + * + * @author luoqiz + * @since 2025/3/12 18:11 + */ +@Component +@RequiredArgsConstructor +public class FileInfoContainer implements Container { + + private final FileService fileService; + + @Override + public String getNamespace() { + return ContainerConstants.FILE_INFO; + } + + @Override + public Map get(Collection ids) { + List list = fileService.listByIds(ids); + return list.stream().collect(Collectors.toMap(FileDO::getId, Function.identity())); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileRecorderImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileRecorderImpl.java new file mode 100644 index 0000000..f2ac23b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileRecorderImpl.java @@ -0,0 +1,127 @@ +package top.wms.admin.system.config.file; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.EscapeUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.recorder.FileRecorder; +import org.dromara.x.file.storage.core.upload.FilePartInfo; +import org.springframework.stereotype.Component; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.system.enums.FileTypeEnum; +import top.wms.admin.system.mapper.FileMapper; +import top.wms.admin.system.mapper.StorageMapper; +import top.wms.admin.system.model.entity.FileDO; +import top.wms.admin.system.model.entity.StorageDO; +import top.continew.starter.core.constant.StringConstants; + +import java.util.Optional; + +/** + * 文件记录实现类 + * + * @author Charles7c + * @since 2023/12/24 22:31 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FileRecorderImpl implements FileRecorder { + + private final FileMapper fileMapper; + private final StorageMapper storageMapper; + private final IdentifierGenerator identifierGenerator; + + @Override + public boolean save(FileInfo fileInfo) { + if (StrUtil.isNotBlank(fileInfo.getObjectType()) && StrUtil.equals("false", fileInfo.getObjectType())) { + return true; + } + FileDO file = new FileDO(); + Number id = identifierGenerator.nextId(fileInfo); + file.setId(id.longValue()); + fileInfo.setId(id.longValue() + ""); + String originalFilename = EscapeUtil.unescape(fileInfo.getOriginalFilename()); + file.setName(StrUtil.contains(originalFilename, StringConstants.DOT) + ? StrUtil.subBefore(originalFilename, StringConstants.DOT, true) + : originalFilename); + file.setUrl(fileInfo.getUrl()); + file.setSize(fileInfo.getSize()); + String absPath = fileInfo.getPath(); + if (absPath.endsWith(StringConstants.SLASH)) { + String tempAbsPath = absPath.substring(0, absPath.length() - 1); + String[] pathArr = tempAbsPath.split(StringConstants.SLASH); + if (pathArr.length > 1) { + file.setParentPath(pathArr[pathArr.length - 1]); + } else { + file.setParentPath(StringConstants.SLASH); + } + } + file.setAbsPath(fileInfo.getPath()); + file.setExtension(fileInfo.getExt()); + file.setType(FileTypeEnum.getByExtension(file.getExtension())); + file.setContentType(fileInfo.getContentType()); + file.setMd5(fileInfo.getHashInfo().getMd5()); + file.setMetadata(JSONUtil.toJsonStr(fileInfo.getMetadata())); + file.setThumbnailUrl(fileInfo.getThUrl()); + file.setThumbnailSize(fileInfo.getThSize()); + file.setThumbnailMetadata(JSONUtil.toJsonStr(fileInfo.getThMetadata())); + StorageDO storage = (StorageDO)fileInfo.getAttr().get(ClassUtil.getClassName(StorageDO.class, false)); + file.setStorageId(storage.getId()); + file.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime())); + file.setUpdateUser(UserContextHolder.getUserId()); + file.setUpdateTime(file.getCreateTime()); + fileMapper.insert(file); + return true; + } + + @Override + public FileInfo getByUrl(String url) { + FileDO file = this.getFileByUrl(url); + if (null == file) { + return null; + } + StorageDO storageDO = storageMapper.lambdaQuery().eq(StorageDO::getId, file.getStorageId()).one(); + return file.toFileInfo(storageDO); + } + + @Override + public boolean delete(String url) { + FileDO file = this.getFileByUrl(url); + return fileMapper.lambdaUpdate().eq(FileDO::getUrl, file.getUrl()).remove(); + } + + @Override + public void update(FileInfo fileInfo) { + /* 不使用分片功能则无需重写 */ + } + + @Override + public void saveFilePart(FilePartInfo filePartInfo) { + /* 不使用分片功能则无需重写 */ + } + + @Override + public void deleteFilePartByUploadId(String s) { + /* 不使用分片功能则无需重写 */ + } + + /** + * 根据 URL 查询文件 + * + * @param url URL + * @return 文件信息 + */ + private FileDO getFileByUrl(String url) { + Optional fileOptional = fileMapper.lambdaQuery().eq(FileDO::getUrl, url).oneOpt(); + return fileOptional.orElseGet(() -> fileMapper.lambdaQuery() + .likeLeft(FileDO::getUrl, StrUtil.subAfter(url, StringConstants.SLASH, true)) + .oneOpt() + .orElse(null)); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileStorageConfigLoader.java b/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileStorageConfigLoader.java new file mode 100644 index 0000000..0405ecf --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/config/file/FileStorageConfigLoader.java @@ -0,0 +1,41 @@ +package top.wms.admin.system.config.file; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.model.query.StorageQuery; +import top.wms.admin.system.model.req.StorageReq; +import top.wms.admin.system.model.resp.StorageResp; +import top.wms.admin.system.service.StorageService; + +import java.util.List; + +/** + * 文件存储配置加载器 + * + * @author Charles7c + * @since 2023/12/24 22:31 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FileStorageConfigLoader implements ApplicationRunner { + + private final StorageService storageService; + + @Override + public void run(ApplicationArguments args) { + StorageQuery query = new StorageQuery(); + query.setStatus(DisEnableStatusEnum.ENABLE); + List storageList = storageService.list(query, null); + if (CollUtil.isEmpty(storageList)) { + return; + } + storageList.forEach(s -> storageService.load(BeanUtil.copyProperties(s, StorageReq.class))); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/config/mail/MailConfigurerImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/config/mail/MailConfigurerImpl.java new file mode 100644 index 0000000..5d42b9d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/config/mail/MailConfigurerImpl.java @@ -0,0 +1,45 @@ +package top.wms.admin.system.config.mail; + +import cn.hutool.core.map.MapUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.system.enums.OptionCategoryEnum; +import top.wms.admin.system.service.OptionService; +import top.continew.starter.messaging.mail.core.MailConfig; +import top.continew.starter.messaging.mail.core.MailConfigurer; + +import java.util.Map; + +/** + * 邮件配置实现 + * + * @author Charles7c + * @since 2024/5/30 22:32 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MailConfigurerImpl implements MailConfigurer { + + private final OptionService optionService; + + @Override + public MailConfig getMailConfig() { + // 查询邮件配置 + Map map = optionService.getByCategory(OptionCategoryEnum.MAIL); + // 封装邮件配置 + MailConfig mailConfig = new MailConfig(); + mailConfig.setProtocol(MapUtil.getStr(map, "MAIL_PROTOCOL")); + mailConfig.setHost(MapUtil.getStr(map, "MAIL_HOST")); + mailConfig.setPort(MapUtil.getInt(map, "MAIL_PORT")); + mailConfig.setUsername(MapUtil.getStr(map, "MAIL_USERNAME")); + mailConfig.setPassword(MapUtil.getStr(map, "MAIL_PASSWORD")); + mailConfig.setSslEnabled(SysConstants.YES.equals(MapUtil.getInt(map, "MAIL_SSL_ENABLED"))); + if (mailConfig.isSslEnabled()) { + mailConfig.setSslPort(MapUtil.getInt(map, "MAIL_SSL_PORT")); + } + return mailConfig; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/FileTypeEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/FileTypeEnum.java new file mode 100644 index 0000000..f931f69 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/FileTypeEnum.java @@ -0,0 +1,69 @@ +package top.wms.admin.system.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 文件类型枚举 + * + * @author Charles7c + * @since 2023/12/23 13:38 + */ +@Getter +@RequiredArgsConstructor +public enum FileTypeEnum implements BaseEnum { + + /** + * 目录 + */ + DIR(0, "目录", Collections.emptyList()), + + /** + * 其他 + */ + UNKNOWN(1, "其他", Collections.emptyList()), + + /** + * 图片 + */ + IMAGE(2, "图片", List + .of("jpg", "jpeg", "png", "gif", "bmp", "webp", "ico", "psd", "tiff", "dwg", "jxr", "apng", "xcf")), + + /** + * 文档 + */ + DOC(3, "文档", List.of("txt", "pdf", "doc", "xls", "ppt", "docx", "xlsx", "pptx")), + + /** + * 视频 + */ + VIDEO(4, "视频", List.of("mp4", "avi", "mkv", "flv", "webm", "wmv", "m4v", "mov", "mpg", "rmvb", "3gp")), + + /** + * 音频 + */ + AUDIO(5, "音频", List.of("mp3", "flac", "wav", "ogg", "midi", "m4a", "aac", "amr", "ac3", "aiff")),; + + private final Integer value; + private final String description; + private final List extensions; + + /** + * 根据扩展名查询 + * + * @param extension 扩展名 + * @return 文件类型 + */ + public static FileTypeEnum getByExtension(String extension) { + return Arrays.stream(FileTypeEnum.values()) + .filter(t -> t.getExtensions().contains(StrUtil.emptyIfNull(extension).toLowerCase())) + .findFirst() + .orElse(FileTypeEnum.UNKNOWN); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/ImportPolicyEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/ImportPolicyEnum.java new file mode 100644 index 0000000..7edc883 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/ImportPolicyEnum.java @@ -0,0 +1,41 @@ +package top.wms.admin.system.enums; + +import cn.hutool.core.collection.CollUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +import java.util.List; + +/** + * 数据导入策略 + * + * @author Kils + * @since 2024-06-17 18:33 + */ +@Getter +@RequiredArgsConstructor +public enum ImportPolicyEnum implements BaseEnum { + + /** + * 跳过该行 + */ + SKIP(1, "跳过该行"), + + /** + * 修改数据 + */ + UPDATE(2, "修改数据"), + + /** + * 停止导入 + */ + EXIT(3, "停止导入"); + + private final Integer value; + private final String description; + + public boolean validate(ImportPolicyEnum importPolicy, String data, List existList) { + return this == importPolicy && CollUtil.isNotEmpty(existList) && existList.contains(data); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/LogStatusEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/LogStatusEnum.java new file mode 100644 index 0000000..8027e5c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/LogStatusEnum.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 操作状态枚举 + * + * @author Charles7c + * @since 2022/12/25 9:09 + */ +@Getter +@RequiredArgsConstructor +public enum LogStatusEnum implements BaseEnum { + + /** + * 成功 + */ + SUCCESS(1, "成功"), + + /** + * 失败 + */ + FAILURE(2, "失败"),; + + private final Integer value; + private final String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/MenuTypeEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/MenuTypeEnum.java new file mode 100644 index 0000000..fd1c157 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/MenuTypeEnum.java @@ -0,0 +1,34 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 菜单类型枚举 + * + * @author Charles7c + * @since 2023/2/15 20:12 + */ +@Getter +@RequiredArgsConstructor +public enum MenuTypeEnum implements BaseEnum { + + /** + * 目录 + */ + DIR(1, "目录"), + + /** + * 菜单 + */ + MENU(2, "菜单"), + + /** + * 按钮 + */ + BUTTON(3, "按钮"),; + + private final Integer value; + private final String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/MessageTemplateEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/MessageTemplateEnum.java new file mode 100644 index 0000000..ca6ae08 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/MessageTemplateEnum.java @@ -0,0 +1,23 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 消息模板枚举 + * + * @author Bull-BCLS + * @since 2023/10/15 19:51 + */ +@Getter +@RequiredArgsConstructor +public enum MessageTemplateEnum { + + /** + * 第三方登录 + */ + SOCIAL_REGISTER("欢迎注册 %s", "尊敬的 %s,欢迎注册使用,请及时配置您的密码。"); + + private final String title; + private final String content; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/MessageTypeEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/MessageTypeEnum.java new file mode 100644 index 0000000..24cf8e2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/MessageTypeEnum.java @@ -0,0 +1,26 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 消息类型枚举 + * + * @author Charles7c + * @since 2023/11/2 20:08 + */ +@Getter +@RequiredArgsConstructor +public enum MessageTypeEnum implements BaseEnum { + + /** + * 安全消息 + */ + SECURITY(1, "安全消息", UiConstants.COLOR_PRIMARY),; + + private final Integer value; + private final String description; + private final String color; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/NoticeScopeEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/NoticeScopeEnum.java new file mode 100644 index 0000000..ee7100e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/NoticeScopeEnum.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 公告通知范围枚举 + * + * @author Charles7c + * @since 2023/8/20 10:55 + */ +@Getter +@RequiredArgsConstructor +public enum NoticeScopeEnum implements BaseEnum { + + /** + * 所有人 + */ + ALL(1, "所有人"), + + /** + * 指定用户 + */ + USER(2, "指定用户"),; + + private final Integer value; + private final String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/NoticeStatusEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/NoticeStatusEnum.java new file mode 100644 index 0000000..5dc5620 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/NoticeStatusEnum.java @@ -0,0 +1,56 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +import java.time.LocalDateTime; + +/** + * 公告状态枚举 + * + * @author Charles7c + * @since 2023/8/20 10:55 + */ +@Getter +@RequiredArgsConstructor +public enum NoticeStatusEnum implements BaseEnum { + + /** + * 待发布 + */ + PENDING_RELEASE(1, "待发布", UiConstants.COLOR_PRIMARY), + + /** + * 已发布 + */ + PUBLISHED(2, "已发布", UiConstants.COLOR_SUCCESS), + + /** + * 已过期 + */ + EXPIRED(3, "已过期", UiConstants.COLOR_ERROR),; + + private final Integer value; + private final String description; + private final String color; + + /** + * 获取公告状态 + * + * @param effectiveTime 生效时间 + * @param terminateTime 终止时间 + * @return 公告状态 + */ + public static NoticeStatusEnum getStatus(LocalDateTime effectiveTime, LocalDateTime terminateTime) { + LocalDateTime now = LocalDateTime.now(); + if (effectiveTime != null && effectiveTime.isAfter(now)) { + return PENDING_RELEASE; + } + if (terminateTime != null && terminateTime.isBefore(now)) { + return EXPIRED; + } + return PUBLISHED; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/OptionCategoryEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/OptionCategoryEnum.java new file mode 100644 index 0000000..5f9b8c6 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/OptionCategoryEnum.java @@ -0,0 +1,30 @@ +package top.wms.admin.system.enums; + +/** + * 参数类别枚举 + * + * @author Charles7c + * @since 2024/11/14 20:00 + */ +public enum OptionCategoryEnum { + + /** + * 系统配置 + */ + SITE, + + /** + * 密码配置 + */ + PASSWORD, + + /** + * 邮箱配置 + */ + MAIL, + + /** + * 登录配置 + */ + LOGIN, +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/PasswordPolicyEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/PasswordPolicyEnum.java new file mode 100644 index 0000000..ca7e1ae --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/PasswordPolicyEnum.java @@ -0,0 +1,181 @@ +package top.wms.admin.system.enums; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.RegexConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.service.OptionService; +import top.wms.admin.system.service.UserPasswordHistoryService; +import top.continew.starter.core.validation.ValidationUtils; + +import java.util.Map; + +/** + * 密码策略枚举 + * + * @author Kils + * @author Charles7c + * @since 2024/5/9 11:25 + */ +@Getter +@RequiredArgsConstructor +public enum PasswordPolicyEnum { + + /** + * 密码错误锁定阈值 + */ + PASSWORD_ERROR_LOCK_COUNT("密码错误锁定阈值取值范围为 %d-%d", SysConstants.NO, 10, "密码错误已达 %d 次,账号锁定 %d 分钟"), + + /** + * 账号锁定时长(分钟) + */ + PASSWORD_ERROR_LOCK_MINUTES("账号锁定时长取值范围为 %d-%d 分钟", 1, 1440, "账号锁定 %d 分钟,请稍后再试"), + + /** + * 密码有效期(天) + */ + PASSWORD_EXPIRATION_DAYS("密码有效期取值范围为 %d-%d 天", SysConstants.NO, 999, null), + + /** + * 密码到期提醒(天) + */ + PASSWORD_EXPIRATION_WARNING_DAYS("密码到期提醒取值范围为 %d-%d 天", SysConstants.NO, 998, null) { + @Override + public void validateRange(int value, Map policyMap) { + if (CollUtil.isEmpty(policyMap)) { + super.validateRange(value, policyMap); + return; + } + Integer passwordExpirationDays = ObjectUtil.defaultIfNull(Convert.toInt(policyMap + .get(PASSWORD_EXPIRATION_DAYS.name())), SpringUtil.getBean(OptionService.class) + .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name())); + if (passwordExpirationDays > SysConstants.NO) { + ValidationUtils.throwIf(value >= passwordExpirationDays, "密码到期提醒时间应小于密码有效期"); + return; + } + super.validateRange(value, policyMap); + } + }, + + /** + * 密码最小长度 + */ + PASSWORD_MIN_LENGTH("密码最小长度取值范围为 %d-%d", 8, 32, "密码最小长度为 %d 个字符") { + @Override + public void validate(String password, int value, UserDO user) { + // 最小长度校验 + ValidationUtils.throwIf(StrUtil.length(password) < value, this.getMsg().formatted(value)); + // 完整校验 + int passwordMaxLength = this.getMax(); + ValidationUtils.throwIf(!ReUtil.isMatch(RegexConstants.PASSWORD_TEMPLATE + .formatted(value, passwordMaxLength), password), "密码长度为 {}-{} 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字", value, passwordMaxLength); + } + }, + + /** + * 密码是否必须包含特殊字符 + */ + PASSWORD_REQUIRE_SYMBOLS("密码是否必须包含特殊字符取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码必须包含特殊字符") { + @Override + public void validateRange(int value, Map policyMap) { + ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription() + .formatted(SysConstants.YES, SysConstants.NO)); + } + + @Override + public void validate(String password, int value, UserDO user) { + ValidationUtils.throwIf(value == SysConstants.YES && !ReUtil + .isMatch(RegexConstants.SPECIAL_CHARACTER, password), this.getMsg()); + } + }, + + /** + * 密码是否允许包含用户名 + */ + PASSWORD_ALLOW_CONTAIN_USERNAME("密码是否允许包含用户名取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码不允许包含正反序用户名") { + @Override + public void validateRange(int value, Map policyMap) { + ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription() + .formatted(SysConstants.YES, SysConstants.NO)); + } + + @Override + public void validate(String password, int value, UserDO user) { + if (value <= SysConstants.NO) { + String username = user.getUsername(); + ValidationUtils.throwIf(StrUtil.containsAnyIgnoreCase(password, username, StrUtil + .reverse(username)), this.getMsg()); + } + } + }, + + /** + * 历史密码重复校验次数 + */ + PASSWORD_REPETITION_TIMES("历史密码重复校验次数取值范围为 %d-%d", 3, 32, "新密码不得与历史前 %d 次密码重复") { + @Override + public void validate(String password, int value, UserDO user) { + UserPasswordHistoryService userPasswordHistoryService = SpringUtil + .getBean(UserPasswordHistoryService.class); + ValidationUtils.throwIf(userPasswordHistoryService.isPasswordReused(user.getId(), password, value), this + .getMsg() + .formatted(value)); + } + },; + + /** + * 描述 + */ + private final String description; + + /** + * 最小值 + */ + private final Integer min; + + /** + * 最大值 + */ + private final Integer max; + + /** + * 提示信息 + */ + private final String msg; + + /** + * 策略类别 + */ + public static final OptionCategoryEnum CATEGORY = OptionCategoryEnum.PASSWORD; + + /** + * 校验取值范围 + * + * @param value 值 + * @param policyMap 策略集合 + */ + public void validateRange(int value, Map policyMap) { + Integer minValue = this.getMin(); + Integer maxValue = this.getMax(); + ValidationUtils.throwIf(value < minValue || value > maxValue, this.getDescription() + .formatted(minValue, maxValue)); + } + + /** + * 校验 + * + * @param password 密码 + * @param value 策略值 + * @param user 用户信息 + */ + public void validate(String password, int value, UserDO user) { + // 无需校验 + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/SocialSourceEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/SocialSourceEnum.java new file mode 100644 index 0000000..abbc254 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/SocialSourceEnum.java @@ -0,0 +1,27 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 第三方账号平台枚举 + * + * @author Charles7c + * @since 2023/10/19 21:22 + */ +@Getter +@RequiredArgsConstructor +public enum SocialSourceEnum { + + /** + * 码云 + */ + GITEE("码云"), + + /** + * GitHub + */ + GITHUB("GitHub"),; + + private final String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/enums/StorageTypeEnum.java b/mes-module-system/src/main/java/top/mes/admin/system/enums/StorageTypeEnum.java new file mode 100644 index 0000000..ca586ad --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/enums/StorageTypeEnum.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 存储类型枚举 + * + * @author Charles7c + * @since 2023/12/27 21:45 + */ +@Getter +@RequiredArgsConstructor +public enum StorageTypeEnum implements BaseEnum { + + /** + * 本地存储 + */ + LOCAL(1, "本地存储"), + + /** + * 对象存储 + */ + OSS(2, "对象存储"); + + private final Integer value; + private final String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/ClientMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/ClientMapper.java new file mode 100644 index 0000000..a919fa8 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/ClientMapper.java @@ -0,0 +1,13 @@ +package top.wms.admin.system.mapper; + +import top.wms.admin.system.model.entity.ClientDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 终端 Mapper + * + * @author KAI + * @since 2024/12/03 16:04 + */ +public interface ClientMapper extends BaseMapper { +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/ConfigMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/ConfigMapper.java new file mode 100644 index 0000000..ce5475d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/ConfigMapper.java @@ -0,0 +1,12 @@ +package top.wms.admin.system.mapper; + +import top.continew.starter.data.mp.base.BaseMapper; +import top.wms.admin.system.model.entity.ConfigDO; + +/** + * 参数配置 Mapper + * + * @author zc + * @since 2025/12/30 12:44 + */ +public interface ConfigMapper extends BaseMapper {} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/DeptMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/DeptMapper.java new file mode 100644 index 0000000..07835bf --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/DeptMapper.java @@ -0,0 +1,13 @@ +package top.wms.admin.system.mapper; + +import top.wms.admin.system.model.entity.DeptDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 部门 Mapper + * + * @author Charles7c + * @since 2023/1/22 17:56 + */ +public interface DeptMapper extends BaseMapper { +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/DictItemMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/DictItemMapper.java new file mode 100644 index 0000000..af0d09b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/DictItemMapper.java @@ -0,0 +1,28 @@ +package top.wms.admin.system.mapper; + +import com.alicp.jetcache.anno.Cached; +import org.apache.ibatis.annotations.Param; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.model.entity.DictItemDO; +import top.continew.starter.data.mp.base.BaseMapper; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; + +import java.util.List; + +/** + * 字典项 Mapper + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +public interface DictItemMapper extends BaseMapper { + + /** + * 根据字典编码查询 + * + * @param dictCode 字典编码 + * @return 字典项列表 + */ + @Cached(key = "#dictCode", name = CacheConstants.DICT_KEY_PREFIX) + List listByDictCode(@Param("dictCode") String dictCode); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/DictMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/DictMapper.java new file mode 100644 index 0000000..853c4e6 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/DictMapper.java @@ -0,0 +1,13 @@ +package top.wms.admin.system.mapper; + +import top.wms.admin.system.model.entity.DictDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 字典 Mapper + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +public interface DictMapper extends BaseMapper { +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/FileMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/FileMapper.java new file mode 100644 index 0000000..d275102 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/FileMapper.java @@ -0,0 +1,25 @@ +package top.wms.admin.system.mapper; + +import org.apache.ibatis.annotations.Select; +import top.wms.admin.system.model.entity.FileDO; +import top.wms.admin.system.model.resp.file.FileStatisticsResp; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; + +/** + * 文件 Mapper + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +public interface FileMapper extends BaseMapper { + + /** + * 查询文件资源统计信息 + * + * @return 文件资源统计信息 + */ + @Select("SELECT type, COUNT(1) number, SUM(size) size FROM sys_file GROUP BY type") + List statistics(); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/LogMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/LogMapper.java new file mode 100644 index 0000000..d0c611a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/LogMapper.java @@ -0,0 +1,48 @@ +package top.wms.admin.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import top.wms.admin.system.model.entity.LogDO; +import top.wms.admin.system.model.resp.log.LogResp; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; + +/** + * 系统日志 Mapper + * + * @author Charles7c + * @since 2022/12/22 21:47 + */ +public interface LogMapper extends BaseMapper { + + /** + * 分页查询列表 + * + * @param page 分页条件 + * @param queryWrapper 查询条件 + * @return 分页列表信息 + */ + IPage selectLogPage(@Param("page") IPage page, + @Param(Constants.WRAPPER) QueryWrapper queryWrapper); + + /** + * 查询列表 + * + * @param queryWrapper 查询条件 + * @return 列表信息 + */ + List selectLogList(@Param(Constants.WRAPPER) QueryWrapper queryWrapper); + + /** + * 查询总数量 + * + * @return 总数量 + */ + @Select("SELECT COUNT(*) FROM sys_log") + Long selectTotalCount(); + +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/MenuMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/MenuMapper.java new file mode 100644 index 0000000..fa703c7 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/MenuMapper.java @@ -0,0 +1,33 @@ +package top.wms.admin.system.mapper; + +import org.apache.ibatis.annotations.Param; +import top.wms.admin.system.model.entity.MenuDO; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; +import java.util.Set; + +/** + * 菜单 Mapper + * + * @author Charles7c + * @since 2023/2/15 20:30 + */ +public interface MenuMapper extends BaseMapper { + + /** + * 根据用户 ID 查询权限码 + * + * @param userId 用户 ID + * @return 权限码集合 + */ + Set selectPermissionByUserId(@Param("userId") Long userId); + + /** + * 根据角色 ID 查询 + * + * @param roleId 角色 ID + * @return 菜单列表 + */ + List selectListByRoleId(@Param("roleId") Long roleId); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/OptionMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/OptionMapper.java new file mode 100644 index 0000000..ec760f3 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/OptionMapper.java @@ -0,0 +1,26 @@ +package top.wms.admin.system.mapper; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import top.wms.admin.system.model.entity.OptionDO; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; + +/** + * 参数 Mapper + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +public interface OptionMapper extends BaseMapper { + + /** + * 根据类别查询 + * + * @param category 类别 + * @return 列表 + */ + @Select("SELECT code, value, default_value FROM sys_option WHERE category = #{category}") + List selectByCategory(@Param("category") String category); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/PeopleEquipmentMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/PeopleEquipmentMapper.java new file mode 100644 index 0000000..6a711bc --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/PeopleEquipmentMapper.java @@ -0,0 +1,16 @@ +package top.wms.admin.system.mapper; + +import org.springframework.stereotype.Repository; +import top.continew.starter.data.mp.base.BaseMapper; +import top.wms.admin.system.model.entity.PeopleEquipmentDO; + +/** + * 人员设备下发信息 Mapper + * + * @author zc + * @since 2025/03/27 14:59 + */ +@Repository +public interface PeopleEquipmentMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleDeptMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleDeptMapper.java new file mode 100644 index 0000000..dba1b58 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleDeptMapper.java @@ -0,0 +1,26 @@ +package top.wms.admin.system.mapper; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import top.wms.admin.system.model.entity.RoleDeptDO; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; + +/** + * 角色和部门 Mapper + * + * @author Charles7c + * @since 2023/2/18 21:57 + */ +public interface RoleDeptMapper extends BaseMapper { + + /** + * 根据角色 ID 查询 + * + * @param roleId 角色 ID + * @return 部门 ID 列表 + */ + @Select("SELECT dept_id FROM sys_role_dept WHERE role_id = #{roleId}") + List selectDeptIdByRoleId(@Param("roleId") Long roleId); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleMapper.java new file mode 100644 index 0000000..a34a16e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleMapper.java @@ -0,0 +1,13 @@ +package top.wms.admin.system.mapper; + +import top.wms.admin.system.model.entity.RoleDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 角色 Mapper + * + * @author Charles7c + * @since 2023/2/8 23:17 + */ +public interface RoleMapper extends BaseMapper { +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleMenuMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleMenuMapper.java new file mode 100644 index 0000000..ebc8a64 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/RoleMenuMapper.java @@ -0,0 +1,23 @@ +package top.wms.admin.system.mapper; + +import top.wms.admin.system.model.entity.RoleMenuDO; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; + +/** + * 角色和菜单 Mapper + * + * @author Charles7c + * @since 2023/2/15 20:30 + */ +public interface RoleMenuMapper extends BaseMapper { + + /** + * 根据角色 ID 列表查询 + * + * @param roleIds 角色 ID 列表 + * @return 菜单 ID 列表 + */ + List selectMenuIdByRoleIds(List roleIds); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/StorageMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/StorageMapper.java new file mode 100644 index 0000000..2779951 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/StorageMapper.java @@ -0,0 +1,13 @@ +package top.wms.admin.system.mapper; + +import top.wms.admin.system.model.entity.StorageDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 存储 Mapper + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +public interface StorageMapper extends BaseMapper { +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserMapper.java new file mode 100644 index 0000000..d58b8aa --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserMapper.java @@ -0,0 +1,106 @@ +package top.wms.admin.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import top.wms.admin.common.config.mybatis.DataPermissionMapper; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.resp.user.UserDetailResp; +import top.continew.starter.extension.datapermission.annotation.DataPermission; +import top.continew.starter.security.crypto.annotation.FieldEncrypt; + +import java.util.List; + +/** + * 用户 Mapper + * + * @author Charles7c + * @since 2022/12/22 21:47 + */ +public interface UserMapper extends DataPermissionMapper { + + /** + * 分页查询列表 + * + * @param page 分页条件 + * @param queryWrapper 查询条件 + * @return 分页列表信息 + */ + @DataPermission(tableAlias = "t1") + IPage selectUserPage(@Param("page") IPage page, + @Param(Constants.WRAPPER) QueryWrapper queryWrapper); + + /** + * 查询列表 + * + * @param queryWrapper 查询条件 + * @return 列表信息 + */ + @DataPermission(tableAlias = "t1") + List selectUserList(@Param(Constants.WRAPPER) QueryWrapper queryWrapper); + + /** + * 根据用户名查询 + * + * @param username 用户名 + * @return 用户信息 + */ + @Select("SELECT * FROM sys_user WHERE username = #{username}") + UserDO selectByUsername(@Param("username") String username); + + /** + * 根据手机号查询 + * + * @param phone 手机号 + * @return 用户信息 + */ + @Select("SELECT * FROM sys_user WHERE phone = #{phone}") + UserDO selectByPhone(@FieldEncrypt @Param("phone") String phone); + + /** + * 根据邮箱查询 + * + * @param email 邮箱 + * @return 用户信息 + */ + @Select("SELECT * FROM sys_user WHERE email = #{email}") + UserDO selectByEmail(@FieldEncrypt @Param("email") String email); + + /** + * 根据 ID 查询昵称 + * + * @param id ID + * @return 昵称 + */ + @Select("SELECT nickname FROM sys_user WHERE id = #{id}") + String selectNicknameById(@Param("id") Long id); + + /** + * 根据邮箱查询数量 + * + * @param email 邮箱 + * @param id ID + * @return 用户数量 + */ + Long selectCountByEmail(@FieldEncrypt @Param("email") String email, @Param("id") Long id); + + /** + * 根据手机号查询数量 + * + * @param phone 手机号 + * @param id ID + * @return 用户数量 + */ + Long selectCountByPhone(@FieldEncrypt @Param("phone") String phone, @Param("id") Long id); + + /** + * 根据卡片号查询 + * + * @param card 卡片号 + * @return 用户信息 + */ + @Select("SELECT * FROM sys_user WHERE card_no = #{card}") + UserDO selectByCard(@Param("card") String card); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserPasswordHistoryMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserPasswordHistoryMapper.java new file mode 100644 index 0000000..21563b2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserPasswordHistoryMapper.java @@ -0,0 +1,22 @@ +package top.wms.admin.system.mapper; + +import org.apache.ibatis.annotations.Param; +import top.wms.admin.system.model.entity.UserPasswordHistoryDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 用户历史密码 Mapper + * + * @author Charles7c + * @since 2024/5/16 21:58 + */ +public interface UserPasswordHistoryMapper extends BaseMapper { + + /** + * 删除过期历史密码 + * + * @param userId 用户 ID + * @param count 保留 N 个历史 + */ + void deleteExpired(@Param("userId") Long userId, @Param("count") int count); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserRoleMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserRoleMapper.java new file mode 100644 index 0000000..b849846 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserRoleMapper.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import org.apache.ibatis.annotations.Param; +import top.wms.admin.system.model.entity.UserRoleDO; +import top.wms.admin.system.model.resp.role.RoleUserResp; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 用户和角色 Mapper + * + * @author Charles7c + * @since 2023/2/13 23:13 + */ +public interface UserRoleMapper extends BaseMapper { + + /** + * 分页查询列表 + * + * @param page 分页条件 + * @param queryWrapper 查询条件 + * @return 分页列表信息 + */ + IPage selectUserPage(@Param("page") IPage page, + @Param(Constants.WRAPPER) QueryWrapper queryWrapper); + +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserSocialMapper.java b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserSocialMapper.java new file mode 100644 index 0000000..2a6298f --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/mapper/UserSocialMapper.java @@ -0,0 +1,23 @@ +package top.wms.admin.system.mapper; + +import org.apache.ibatis.annotations.Param; +import top.wms.admin.system.model.entity.UserSocialDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 用户社会化关联 Mapper + * + * @author Charles7c + * @since 2023/10/11 22:10 + */ +public interface UserSocialMapper extends BaseMapper { + + /** + * 根据来源和开放 ID 查询 + * + * @param source 来源 + * @param openId 开放 ID + * @return 用户社会化关联信息 + */ + UserSocialDO selectBySourceAndOpenId(@Param("source") String source, @Param("openId") String openId); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/ClientDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/ClientDO.java new file mode 100644 index 0000000..9d988b2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/ClientDO.java @@ -0,0 +1,67 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; +import java.util.List; + +/** + * 终端实体 + * + * @author KAI + * @author Charles7c + * @since 2024/12/03 16:04 + */ +@Data +@TableName(value = "sys_client", autoResultMap = true) +public class ClientDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 终端 ID + */ + private String clientId; + + /** + * 终端 Key + */ + private String clientKey; + + /** + * 终端秘钥 + */ + private String clientSecret; + + /** + * 登录类型 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authType; + + /** + * 终端类型 + */ + private String clientType; + + /** + * Token 最低活跃频率(单位:秒,-1:不限制,永不冻结) + */ + private Long activeTimeout; + + /** + * Token 有效期(单位:秒,-1:永不过期) + */ + private Long timeout; + + /** + * 状态 + */ + private DisEnableStatusEnum status; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/ConfigDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/ConfigDO.java new file mode 100644 index 0000000..b16aa20 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/ConfigDO.java @@ -0,0 +1,43 @@ +package top.wms.admin.system.model.entity; + +import lombok.Data; + +import com.baomidou.mybatisplus.annotation.TableName; + +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; + +/** + * 参数配置实体 + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Data +@TableName("sys_config") +public class ConfigDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 参数名称 + */ + private String configName; + + /** + * 参数键名 + */ + private String configKey; + + /** + * 参数键值 + */ + private String configValue; + + /** + * 备注 + */ + private String remark; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DeptDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DeptDO.java new file mode 100644 index 0000000..a9d619c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DeptDO.java @@ -0,0 +1,57 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; + +/** + * 部门实体 + * + * @author Charles7c + * @since 2023/1/22 13:50 + */ +@Data +@TableName("sys_dept") +public class DeptDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + private String name; + + /** + * 上级部门 ID + */ + private Long parentId; + + /** + * 祖级列表 + */ + private String ancestors; + + /** + * 描述 + */ + private String description; + + /** + * 排序 + */ + private Integer sort; + + /** + * 状态 + */ + private DisEnableStatusEnum status; + + /** + * 是否为系统内置数据 + */ + private Boolean isSystem; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DictDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DictDO.java new file mode 100644 index 0000000..0117e39 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DictDO.java @@ -0,0 +1,43 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.continew.starter.extension.crud.annotation.DictField; +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; + +/** + * 字典实体 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@DictField(valueKey = "code") +@TableName("sys_dict") +public class DictDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 描述 + */ + private String description; + + /** + * 是否为系统内置数据 + */ + private Boolean isSystem; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DictItemDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DictItemDO.java new file mode 100644 index 0000000..943395c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/DictItemDO.java @@ -0,0 +1,57 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; + +/** + * 字典项实体 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@TableName("sys_dict_item") +public class DictItemDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标签 + */ + private String label; + + /** + * 值 + */ + private String value; + + /** + * 标签颜色 + */ + private String color; + + /** + * 排序 + */ + private Integer sort; + + /** + * 描述 + */ + private String description; + + /** + * 状态 + */ + private DisEnableStatusEnum status; + + /** + * 字典ID + */ + private Long dictId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/FileDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/FileDO.java new file mode 100644 index 0000000..4020d14 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/FileDO.java @@ -0,0 +1,164 @@ +package top.wms.admin.system.model.entity; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.SneakyThrows; +import org.dromara.x.file.storage.core.FileInfo; +import top.wms.admin.common.model.entity.BaseDO; +import top.wms.admin.system.enums.FileTypeEnum; +import top.wms.admin.system.enums.StorageTypeEnum; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.StrUtils; + +import java.io.Serial; +import java.net.URL; +import java.util.Map; + +/** + * 文件实体 + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +@Data +@TableName("sys_file") +public class FileDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + private String name; + + /** + * 大小(字节) + */ + private Long size; + + /** + * URL + */ + private String url; + + /** + * 上级目录 + */ + private String parentPath; + + /** + * 绝对路径 + */ + private String absPath; + + /** + * 扩展名 + */ + private String extension; + + /** + * 内容类型 + */ + private String contentType; + + /** + * 类型 + */ + private FileTypeEnum type; + + /** + * MD5 值 + */ + private String md5; + + /** + * 元数据 + */ + private String metadata; + + /** + * 缩略图大小(字节) + */ + private Long thumbnailSize; + + /** + * 缩略图 URL + */ + private String thumbnailUrl; + + /** + * 缩略图元数据 + */ + private String thumbnailMetadata; + + /** + * 存储 ID + */ + private Long storageId; + + /** + * 转换为 X-File-Storage 文件信息对象 + * + * @param storageDO 存储桶信息 + * @return X-File-Storage 文件信息对象 + */ + public FileInfo toFileInfo(StorageDO storageDO) { + FileInfo fileInfo = new FileInfo(); + fileInfo.setUrl(this.url); + fileInfo.setSize(this.size); + fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH) + ? StrUtil.subAfter(this.url, StringConstants.SLASH, true) + : this.url); + fileInfo.setOriginalFilename(StrUtils + .blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex)); + fileInfo.setBasePath(StringConstants.EMPTY); + // 优化 path 处理 + fileInfo.setPath(extractRelativePath(this.url, storageDO)); + + fileInfo.setExt(this.extension); + fileInfo.setPlatform(storageDO.getCode()); + fileInfo.setThUrl(this.thumbnailUrl); + fileInfo.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH) + ? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true) + : this.thumbnailUrl); + fileInfo.setThSize(this.thumbnailSize); + if (StrUtil.isNotBlank(this.thumbnailMetadata)) { + fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class)); + } + if (StrUtil.isNotBlank(this.metadata)) { + fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class)); + } + return fileInfo; + } + + /** + * 将文件路径处理成资源路径 + * 例如: + * http://domain.cn/bucketName/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/ + * http://bucketName.domain.cn/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/ + * + * @param url 文件路径 + * @param storageDO 存储桶信息 + * @return + */ + @SneakyThrows + private static String extractRelativePath(String url, StorageDO storageDO) { + url = StrUtil.subBefore(url, StringConstants.SLASH, true) + StringConstants.SLASH; + if (storageDO.getType().equals(StorageTypeEnum.LOCAL)) { + return url; + } + // 提取 URL 中的路径部分 + String fullPath = new URL(url).getPath(); + // 移除开头的斜杠 + String relativePath = fullPath.startsWith(StringConstants.SLASH) ? fullPath.substring(1) : fullPath; + // 如果路径以 bucketName 开头,则移除 bucketName 例如: bucketName/2024/11/27/ -> 2024/11/27/ + if (relativePath.startsWith(storageDO.getBucketName())) { + return StrUtil.split(relativePath, storageDO.getBucketName()).get(1); + } + return relativePath; + } + +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/LogDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/LogDO.java new file mode 100644 index 0000000..2002fda --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/LogDO.java @@ -0,0 +1,125 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.system.enums.LogStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 系统日志实体 + * + * @author Charles7c + * @since 2022/12/25 9:11 + */ +@Data +@TableName("sys_log") +public class LogDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + + /** + * 链路 ID + */ + private String traceId; + + /** + * 日志描述 + */ + private String description; + + /** + * 所属模块 + */ + private String module; + + /** + * 请求 URL + */ + private String requestUrl; + + /** + * 请求方式 + */ + private String requestMethod; + + /** + * 请求头 + */ + private String requestHeaders; + + /** + * 请求体 + */ + private String requestBody; + + /** + * 状态码 + */ + private Integer statusCode; + + /** + * 响应头 + */ + private String responseHeaders; + + /** + * 响应体 + */ + private String responseBody; + + /** + * 耗时(ms) + */ + private Long timeTaken; + + /** + * IP + */ + private String ip; + + /** + * IP 归属地 + */ + private String address; + + /** + * 浏览器 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 状态 + */ + private LogStatusEnum status; + + /** + * 错误信息 + */ + private String errorMsg; + + /** + * 创建人 + */ + private Long createUser; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/MenuDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/MenuDO.java new file mode 100644 index 0000000..7f5e9b2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/MenuDO.java @@ -0,0 +1,93 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.MenuTypeEnum; +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; + +/** + * 菜单实体 + * + * @author Charles7c + * @since 2023/2/15 20:14 + */ +@Data +@TableName("sys_menu") +public class MenuDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标题 + */ + private String title; + + /** + * 上级菜单 ID + */ + private Long parentId; + + /** + * 类型 + */ + private MenuTypeEnum type; + + /** + * 路由地址 + */ + private String path; + + /** + * 组件名称 + */ + private String name; + + /** + * 组件路径 + */ + private String component; + + /** + * 重定向地址 + */ + private String redirect; + + /** + * 图标 + */ + private String icon; + + /** + * 是否外链 + */ + private Boolean isExternal; + + /** + * 是否缓存 + */ + private Boolean isCache; + + /** + * 是否隐藏 + */ + private Boolean isHidden; + + /** + * 权限标识 + */ + private String permission; + + /** + * 排序 + */ + private Integer sort; + + /** + * 状态 + */ + private DisEnableStatusEnum status; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/OptionDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/OptionDO.java new file mode 100644 index 0000000..b55924c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/OptionDO.java @@ -0,0 +1,51 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.model.entity.BaseUpdateDO; + +import java.io.Serial; + +/** + * 参数实体 + * + * @author Bull-BCLS + * @since 2023/8/26 19:20 + */ +@Data +@TableName("sys_option") +public class OptionDO extends BaseUpdateDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 类别 + */ + private String category; + + /** + * 名称 + */ + private String name; + + /** + * 键 + */ + private String code; + + /** + * 值 + */ + private String value; + + /** + * 默认值 + */ + private String defaultValue; + + /** + * 描述 + */ + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/PeopleEquipmentDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/PeopleEquipmentDO.java new file mode 100644 index 0000000..29aee53 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/PeopleEquipmentDO.java @@ -0,0 +1,53 @@ +package top.wms.admin.system.model.entity; + +import lombok.Data; + +import com.baomidou.mybatisplus.annotation.TableName; + +import top.continew.starter.extension.crud.model.entity.BaseIdDO; + +import java.io.Serial; + +/** + * 人员设备下发信息实体 + * + * @author zc + * @since 2025/03/27 14:59 + */ +@Data +@TableName("sys_people_equipment") +public class PeopleEquipmentDO extends BaseIdDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 人员Id + */ + private Long peopleId; + + /** + * 设备Id + */ + private Long equipmentId; + + /** + * 人员编号对接 + */ + private String guid; + + /** + * 人像对接 + */ + private String faceGuid; + + /** + * 访客Id + */ + private Long visitorId; + + /** + * 三方人员id + */ + private String otherId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleDO.java new file mode 100644 index 0000000..9027995 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleDO.java @@ -0,0 +1,64 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.enums.DataScopeEnum; +import top.continew.starter.extension.crud.annotation.DictField; +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; + +/** + * 角色实体 + * + * @author Charles7c + * @since 2023/2/8 22:54 + */ +@Data +@DictField +@TableName("sys_role") +public class RoleDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 数据权限 + */ + private DataScopeEnum dataScope; + + /** + * 描述 + */ + private String description; + + /** + * 排序 + */ + private Integer sort; + + /** + * 是否为系统内置数据 + */ + private Boolean isSystem; + + /** + * 菜单选择是否父子节点关联 + */ + private Boolean menuCheckStrictly; + + /** + * 部门选择是否父子节点关联 + */ + private Boolean deptCheckStrictly; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleDeptDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleDeptDO.java new file mode 100644 index 0000000..5b347c4 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleDeptDO.java @@ -0,0 +1,38 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 角色和部门实体 + * + * @author Charles7c + * @since 2023/2/18 21:57 + */ +@Data +@NoArgsConstructor +@TableName("sys_role_dept") +public class RoleDeptDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 角色 ID + */ + private Long roleId; + + /** + * 部门 ID + */ + private Long deptId; + + public RoleDeptDO(Long roleId, Long deptId) { + this.roleId = roleId; + this.deptId = deptId; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleMenuDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleMenuDO.java new file mode 100644 index 0000000..471440d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/RoleMenuDO.java @@ -0,0 +1,38 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 角色和菜单实体 + * + * @author Charles7c + * @since 2023/2/15 20:20 + */ +@Data +@NoArgsConstructor +@TableName("sys_role_menu") +public class RoleMenuDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 角色 ID + */ + private Long roleId; + + /** + * 菜单 ID + */ + private Long menuId; + + public RoleMenuDO(Long roleId, Long menuId) { + this.roleId = roleId; + this.menuId = menuId; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/StorageDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/StorageDO.java new file mode 100644 index 0000000..76fd894 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/StorageDO.java @@ -0,0 +1,86 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.StorageTypeEnum; +import top.wms.admin.common.model.entity.BaseDO; +import top.continew.starter.security.crypto.annotation.FieldEncrypt; + +import java.io.Serial; + +/** + * 存储实体 + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +@Data +@TableName("sys_storage") +public class StorageDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + private String name; + + /** + * 编码 + */ + private String code; + + /** + * 类型 + */ + private StorageTypeEnum type; + + /** + * Access Key + */ + @FieldEncrypt + private String accessKey; + + /** + * Secret Key + */ + @FieldEncrypt + private String secretKey; + + /** + * Endpoint + */ + private String endpoint; + + /** + * Bucket + */ + private String bucketName; + + /** + * 域名 + */ + private String domain; + + /** + * 描述 + */ + private String description; + + /** + * 是否为默认存储 + */ + private Boolean isDefault; + + /** + * 排序 + */ + private Integer sort; + + /** + * 状态 + */ + private DisEnableStatusEnum status; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserDO.java new file mode 100644 index 0000000..3bcea43 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserDO.java @@ -0,0 +1,95 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.enums.GenderEnum; +import top.continew.starter.extension.crud.annotation.DictField; +import top.wms.admin.common.model.entity.BaseDO; +import top.continew.starter.security.crypto.annotation.FieldEncrypt; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** + * 用户实体 + * + * @author Charles7c + * @since 2022/12/21 20:42 + */ +@Data +@DictField(labelKey = "nickname", extraKeys = {"username"}) +@TableName("sys_user") +public class UserDO extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + // @FieldEncrypt(encryptor = BCryptEncryptor.class) + private String password; + + /* + * 卡号 + * */ + private String cardNo; + + /** + * 性别 + */ + private GenderEnum gender; + + /** + * 邮箱 + */ + @FieldEncrypt + @TableField(insertStrategy = FieldStrategy.NOT_EMPTY) + private String email; + + /** + * 手机号码 + */ + @FieldEncrypt + @TableField(insertStrategy = FieldStrategy.NOT_EMPTY) + private String phone; + + /** + * 头像地址 + */ + private String avatar; + + /** + * 描述 + */ + private String description; + + /** + * 状态 + */ + private DisEnableStatusEnum status; + + /** + * 是否为系统内置数据 + */ + private Boolean isSystem; + + /** + * 最后一次修改密码时间 + */ + private LocalDateTime pwdResetTime; + + /** + * 设备ID + */ + @TableField(exist = false) + private Long equipmentId; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserPasswordHistoryDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserPasswordHistoryDO.java new file mode 100644 index 0000000..4e0fb4c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserPasswordHistoryDO.java @@ -0,0 +1,51 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户历史密码实体 + * + * @author Charles7c + * @since 2024/5/16 21:58 + */ +@Data +@NoArgsConstructor +@TableName("sys_user_password_history") +public class UserPasswordHistoryDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 用户 ID + */ + private Long userId; + + /** + * 密码 + */ + private String password; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + public UserPasswordHistoryDO(Long userId, String password) { + this.userId = userId; + this.password = password; + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserRoleDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserRoleDO.java new file mode 100644 index 0000000..264305c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserRoleDO.java @@ -0,0 +1,45 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户和角色实体 + * + * @author Charles7c + * @since 2023/2/13 23:13 + */ +@Data +@NoArgsConstructor +@TableName("sys_user_role") +public class UserRoleDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + + /** + * 用户 ID + */ + private Long userId; + + /** + * 角色 ID + */ + private Long roleId; + + public UserRoleDO(Long userId, Long roleId) { + this.userId = userId; + this.roleId = roleId; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserSocialDO.java b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserSocialDO.java new file mode 100644 index 0000000..75a117a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/entity/UserSocialDO.java @@ -0,0 +1,62 @@ +package top.wms.admin.system.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户社会化关联实体 + * + * @author Charles7c + * @since 2023/10/11 22:10 + */ +@Data +@TableName("sys_user_social") +public class UserSocialDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + + /** + * 用户 ID + */ + private Long userId; + + /** + * 来源 + */ + private String source; + + /** + * 开放 ID + */ + private String openId; + + /** + * 附加信息 + */ + private String metaJson; + + /** + * 最后登录时间 + */ + private LocalDateTime lastLoginTime; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/ClientQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/ClientQuery.java new file mode 100644 index 0000000..1a09733 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/ClientQuery.java @@ -0,0 +1,57 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 终端查询条件 + * + * @author KAI + * @author Charles7c + * @since 2024/12/03 16:04 + */ +@Data +@Schema(description = "终端查询条件") +public class ClientQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 终端 Key + */ + @Schema(description = "终端 Key", example = "PC") + private String clientKey; + + /** + * 终端秘钥 + */ + @Schema(description = "终端秘钥", example = "dd77ab1e353a027e0d60ce3b151e8642") + private String clientSecret; + + /** + * 认证类型 + */ + @Schema(description = "认证类型", example = "ACCOUNT") + @Query(type = QueryType.IN) + private List authType; + + /** + * 终端类型 + */ + @Schema(description = "终端类型", example = "PC") + private String clientType; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/ConfigQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/ConfigQuery.java new file mode 100644 index 0000000..734a5f1 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/ConfigQuery.java @@ -0,0 +1,53 @@ +package top.wms.admin.system.model.query; + +import cn.hutool.core.date.DatePattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import org.springframework.format.annotation.DateTimeFormat; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; +import java.time.*; +import java.util.Date; +import java.util.List; + +/** + * 参数配置查询条件 + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Data +@Schema(description = "参数配置查询条件") +public class ConfigQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 参数名称 + */ + @Schema(description = "参数名称") + @Query(type = QueryType.LIKE) + private String configName; + + /** + * 参数键名 + */ + @Schema(description = "参数键名") + @Query(type = QueryType.EQ) + private String configKey; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08,2023-08-08") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @Size(max = 2, message = "创建时间必须是一个范围") + private List createTime; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/DeptQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/DeptQuery.java new file mode 100644 index 0000000..881eb0b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/DeptQuery.java @@ -0,0 +1,37 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 部门查询条件 + * + * @author Charles7c + * @since 2023/1/22 17:52 + */ +@Data +@Schema(description = "部门查询条件") +public class DeptQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关键词 + */ + @Schema(description = "关键词", example = "测试部") + @Query(columns = {"name", "description"}, type = QueryType.LIKE) + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/DictItemQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/DictItemQuery.java new file mode 100644 index 0000000..03f37db --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/DictItemQuery.java @@ -0,0 +1,43 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 字典项查询条件 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@Schema(description = "字典项查询条件") +public class DictItemQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关键词 + */ + @Schema(description = "关键词") + @Query(columns = {"label", "description"}, type = QueryType.LIKE) + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 字典 ID + */ + @Schema(description = "字典 ID") + private Long dictId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/DictQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/DictQuery.java new file mode 100644 index 0000000..6249a52 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/DictQuery.java @@ -0,0 +1,30 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 字典查询条件 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@Schema(description = "字典查询条件") +public class DictQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关键词 + */ + @Schema(description = "关键词") + @Query(columns = {"name", "code", "description"}, type = QueryType.LIKE) + private String description; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/FileQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/FileQuery.java new file mode 100644 index 0000000..f6f26a7 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/FileQuery.java @@ -0,0 +1,44 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.system.enums.FileTypeEnum; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 文件查询条件 + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +@Data +@Schema(description = "文件查询条件") +public class FileQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "图片") + @Query(type = QueryType.LIKE) + private String name; + + /** + * 绝对路径 + */ + @Schema(description = "绝对路径", example = "/2025") + @Query(type = QueryType.EQ) + private String absPath; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + private FileTypeEnum type; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/LogQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/LogQuery.java new file mode 100644 index 0000000..6fc8c5d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/LogQuery.java @@ -0,0 +1,65 @@ +package top.wms.admin.system.model.query; + +import cn.hutool.core.date.DatePattern; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 日志查询条件 + * + * @author Charles7c + * @since 2023/1/15 11:43 + */ +@Data +@Schema(description = "日志查询条件") +public class LogQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 日志描述 + */ + @Schema(description = "日志描述", example = "新增数据") + private String description; + + /** + * 所属模块 + */ + @Schema(description = "所属模块", example = "所属模块") + private String module; + + /** + * IP + */ + @Schema(description = "IP", example = "") + private String ip; + + /** + * 操作人 + */ + @Schema(description = "操作人", example = "admin") + private String createUserString; + + /** + * 操作时间 + */ + @Schema(description = "操作时间", example = "2023-08-08 00:00:00,2023-08-08 23:59:59") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @Size(max = 2, message = "操作时间必须是一个范围") + private List createTime; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/MenuQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/MenuQuery.java new file mode 100644 index 0000000..e9f8c3f --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/MenuQuery.java @@ -0,0 +1,43 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 菜单查询条件 + * + * @author Charles7c + * @since 2023/2/15 20:21 + */ +@Data +@NoArgsConstructor +@Schema(description = "菜单查询条件") +public class MenuQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标题 + */ + @Schema(description = "标题", example = "用户管理") + @Query(type = QueryType.LIKE) + private String title; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + public MenuQuery(DisEnableStatusEnum status) { + this.status = status; + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/OptionQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/OptionQuery.java new file mode 100644 index 0000000..d079e84 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/OptionQuery.java @@ -0,0 +1,40 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.system.enums.OptionCategoryEnum; +import top.continew.starter.core.validation.constraints.EnumValue; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 参数查询条件 + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +@Data +@Schema(description = "参数查询条件") +public class OptionQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 键列表 + */ + @Schema(description = "键列表", example = "SITE_TITLE,SITE_COPYRIGHT") + @Query(type = QueryType.IN) + private List code; + + /** + * 类别 + */ + @Schema(description = "类别", example = "SITE") + @EnumValue(value = OptionCategoryEnum.class, message = "类别非法") + private String category; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/PeopleEquipmentQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/PeopleEquipmentQuery.java new file mode 100644 index 0000000..a061b3d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/PeopleEquipmentQuery.java @@ -0,0 +1,22 @@ +package top.wms.admin.system.model.query; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 人员设备下发信息查询条件 + * + * @author zc + * @since 2025/03/27 14:59 + */ +@Data +@Schema(description = "人员设备下发信息查询条件") +public class PeopleEquipmentQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/RoleQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/RoleQuery.java new file mode 100644 index 0000000..746b807 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/RoleQuery.java @@ -0,0 +1,30 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 角色查询条件 + * + * @author Charles7c + * @since 2023/2/8 23:04 + */ +@Data +@Schema(description = "角色查询条件") +public class RoleQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关键词 + */ + @Schema(description = "关键词", example = "测试人员") + @Query(columns = {"name", "code", "description"}, type = QueryType.LIKE) + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/RoleUserQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/RoleUserQuery.java new file mode 100644 index 0000000..52f3efc --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/RoleUserQuery.java @@ -0,0 +1,33 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 角色关联用户查询条件 + * + * @author Charles7c + * @since 2025/2/5 22:01 + */ +@Data +@Schema(description = "角色关联用户查询条件") +public class RoleUserQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 角色 ID + */ + @Schema(description = "角色 ID", example = "1") + private Long roleId; + + /** + * 关键词 + */ + @Schema(description = "关键词", example = "zhangsan") + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/StorageQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/StorageQuery.java new file mode 100644 index 0000000..202286c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/StorageQuery.java @@ -0,0 +1,44 @@ +package top.wms.admin.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.StorageTypeEnum; +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 存储查询条件 + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +@Data +@Schema(description = "存储查询条件") +public class StorageQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关键词 + */ + @Schema(description = "关键词", example = "本地存储") + @Query(columns = {"name", "code", "description"}, type = QueryType.LIKE) + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + private StorageTypeEnum type; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/query/UserQuery.java b/mes-module-system/src/main/java/top/mes/admin/system/model/query/UserQuery.java new file mode 100644 index 0000000..65eb18d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/query/UserQuery.java @@ -0,0 +1,75 @@ +package top.wms.admin.system.model.query; + +import cn.hutool.core.date.DatePattern; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * 用户查询条件 + * + * @author Charles7c + * @since 2023/2/20 21:01 + */ +@Data +@Schema(description = "用户查询条件") +public class UserQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 关键词 + */ + @Schema(description = "关键词", example = "zhangsan") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 00:00:00,2023-08-08 23:59:59") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @Size(max = 2, message = "创建时间必须是一个范围") + private List createTime; + + /** + * 部门 ID + */ + @Schema(description = "部门 ID", example = "1") + private Long deptId; + + /** + * 用户 ID 列表 + */ + @Schema(description = "用户 ID 列表", example = "[1,2,3]") + private List userIds; + + /** + * 角色 ID + *

用于在角色授权用户时,过滤掉已经分配给该角色的用户

+ */ + @Schema(description = "角色 ID", example = "1") + private Long roleId; + + /** + * 设备ID + */ + @Schema(description = "设备ID") + private Long equipmentId; + + @Schema(description = "卡号") + private String cardNo; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/ClientReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/ClientReq.java new file mode 100644 index 0000000..0ddc737 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/ClientReq.java @@ -0,0 +1,82 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 创建或修改终端参数 + * + * @author KAI + * @author Charles7c + * @since 2024/12/03 16:04 + */ +@Data +@Schema(description = "创建或修改终端参数") +public class ClientReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 终端 Key + */ + @Schema(description = "终端 Key", example = "PC") + @NotBlank(message = "终端Key不能为空") + @Length(max = 32, message = "终端Key长度不能超过 {max} 个字符") + private String clientKey; + + /** + * 终端秘钥 + */ + @Schema(description = "终端秘钥", example = "dd77ab1e353a027e0d60ce3b151e8642") + @NotBlank(message = "终端秘钥不能为空") + @Length(max = 255, message = "终端秘钥长度不能超过 {max} 个字符") + private String clientSecret; + + /** + * 认证类型 + */ + @Schema(description = "认证类型", example = "ACCOUNT") + @NotEmpty(message = "认证类型不能为空") + private List authType; + + /** + * 终端类型 + */ + @Schema(description = "终端类型", example = "PC") + @NotBlank(message = "终端类型不能为空") + @Length(max = 32, message = "终端类型长度不能超过 {max} 个字符") + private String clientType; + + /** + * Token 最低活跃频率(单位:秒,-1:不限制,永不冻结) + */ + @Schema(description = "Token 最低活跃频率(单位:秒,-1:不限制,永不冻结)", example = "1800") + private Long activeTimeout; + + /** + * Token 有效期(单位:秒,-1:永不过期) + */ + @Schema(description = "Token 有效期(单位:秒,-1:永不过期)", example = "86400") + private Long timeout; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 终端 ID + */ + @Schema(hidden = true) + private String clientId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/ConfigReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/ConfigReq.java new file mode 100644 index 0000000..9a2bedb --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/ConfigReq.java @@ -0,0 +1,57 @@ +package top.wms.admin.system.model.req; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; +import java.time.*; + +/** + * 创建或修改参数配置参数 + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Data +@Schema(description = "创建或修改参数配置参数") +public class ConfigReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 参数名称 + */ + @Schema(description = "参数名称") + @NotNull + @Length(max = 100, message = "参数名称长度不能超过 {max} 个字符") + private String configName; + + /** + * 参数键名 + */ + @Schema(description = "参数键名") + @NotNull + @Length(max = 100, message = "参数键名长度不能超过 {max} 个字符") + private String configKey; + + /** + * 参数值 + */ + @Schema(description = "参数值") + @NotNull + @Length(max = 100, message = "参数值长度不能超过 {max} 个字符") + private String configValue; + + /** + * 备注 + */ + @Schema(description = "备注") + @Length(max = 255, message = "备注长度不能超过 {max} 个字符") + private String remark; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/DeptReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/DeptReq.java new file mode 100644 index 0000000..6674c75 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/DeptReq.java @@ -0,0 +1,67 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 创建或修改部门参数 + * + * @author Charles7c + * @since 2023/1/24 00:21 + */ +@Data +@Schema(description = "创建或修改部门参数") +public class DeptReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 上级部门 ID + */ + @Schema(description = "上级部门 ID", example = "2") + @NotNull(message = "上级部门不能为空") + private Long parentId; + + /** + * 名称 + */ + @Schema(description = "名称", example = "测试部") + @NotBlank(message = "名称不能为空") + @Length(max = 30, message = "名称长度不能超过 {max} 个字符") + private String name; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + @Min(value = 1, message = "排序最小值为 {value}") + private Integer sort; + + /** + * 描述 + */ + @Schema(description = "描述", example = "测试部描述信息") + @Length(max = 200, message = "描述长度不能超过 {max} 个字符") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 祖级列表 + */ + @Schema(hidden = true) + private String ancestors; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/DictItemReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/DictItemReq.java new file mode 100644 index 0000000..30a4426 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/DictItemReq.java @@ -0,0 +1,76 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 创建或修改字典项参数 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@Schema(description = "创建或修改字典项参数") +public class DictItemReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标签 + */ + @Schema(description = "标签", example = "通知") + @NotBlank(message = "标签不能为空") + @Length(max = 30, message = "标签长度不能超过 {max} 个字符") + private String label; + + /** + * 值 + */ + @Schema(description = "值", example = "1") + @NotBlank(message = "值不能为空") + @Length(max = 30, message = "值长度不能超过 {max} 个字符") + private String value; + + /** + * 标签颜色 + */ + @Schema(description = "标签颜色", example = "blue") + @Length(max = 30, message = "标签颜色长度不能超过 {max} 个字符") + private String color; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + @Min(value = 1, message = "排序最小值为 {value}") + private Integer sort; + + /** + * 描述 + */ + @Schema(description = "描述", example = "通知描述信息") + @Length(max = 200, message = "描述长度不能超过 {max} 个字符") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 所属字典 + */ + @Schema(description = "所属字典", example = "1") + @NotNull(message = "所属字典不能为空") + private Long dictId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/DictReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/DictReq.java new file mode 100644 index 0000000..faa0d2e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/DictReq.java @@ -0,0 +1,48 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.constant.RegexConstants; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 创建或修改字典参数 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@Schema(description = "创建或修改字典参数") +public class DictReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "公告类型") + @NotBlank(message = "名称不能为空") + @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "notice_type") + @NotBlank(message = "编码不能为空") + @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") + private String code; + + /** + * 描述 + */ + @Schema(description = "描述", example = "公告类型描述信息") + @Length(max = 200, message = "描述长度不能超过 {max} 个字符") + private String description; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/FileReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/FileReq.java new file mode 100644 index 0000000..75269a1 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/FileReq.java @@ -0,0 +1,31 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 修改文件参数 + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +@Data +@Schema(description = "修改文件参数") +public class FileReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "test123") + @NotBlank(message = "文件名称不能为空") + @Length(max = 255, message = "文件名称长度不能超过 {max} 个字符") + private String name; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/MenuReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/MenuReq.java new file mode 100644 index 0000000..d227c21 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/MenuReq.java @@ -0,0 +1,122 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.MenuTypeEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 创建或修改菜单参数 + * + * @author Charles7c + * @since 2023/2/15 20:21 + */ +@Data +@Schema(description = "创建或修改菜单参数") +public class MenuReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + @NotNull(message = "类型非法") + private MenuTypeEnum type; + + /** + * 图标 + */ + @Schema(description = "图标", example = "user") + @Length(max = 50, message = "图标长度不能超过 {max} 个字符") + private String icon; + + /** + * 标题 + */ + @Schema(description = "标题", example = "用户管理") + @NotBlank(message = "标题不能为空") + @Length(max = 30, message = "标题长度不能超过 {max} 个字符") + private String title; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + @NotNull(message = "排序不能为空") + @Min(value = 1, message = "排序最小值为 {value}") + private Integer sort; + + /** + * 权限标识 + */ + @Schema(description = "权限标识", example = "system:user:list") + @Length(max = 100, message = "权限标识长度不能超过 {max} 个字符") + private String permission; + + /** + * 路由地址 + */ + @Schema(description = "路由地址", example = "/system/user") + @Length(max = 255, message = "路由地址长度不能超过 {max} 个字符") + private String path; + + /** + * 组件名称 + */ + @Schema(description = "组件名称", example = "User") + @Length(max = 50, message = "组件名称长度不能超过 {max} 个字符") + private String name; + + /** + * 组件路径 + */ + @Schema(description = "组件路径", example = "/system/user/index") + @Length(max = 255, message = "组件路径长度不能超过 {max} 个字符") + private String component; + + /** + * 重定向地址 + */ + @Schema(description = "重定向地址") + private String redirect; + + /** + * 是否外链 + */ + @Schema(description = "是否外链", example = "false") + private Boolean isExternal; + + /** + * 是否缓存 + */ + @Schema(description = "是否缓存", example = "false") + private Boolean isCache; + + /** + * 是否隐藏 + */ + @Schema(description = "是否隐藏", example = "false") + private Boolean isHidden; + + /** + * 上级菜单 ID + */ + @Schema(description = "上级菜单 ID", example = "1000") + private Long parentId; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @NotNull(message = "状态非法") + private DisEnableStatusEnum status; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/OptionReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/OptionReq.java new file mode 100644 index 0000000..c6bc9b1 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/OptionReq.java @@ -0,0 +1,45 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 修改参数参数 + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +@Data +@Schema(description = "修改参数参数") +public class OptionReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + @NotNull(message = "ID不能为空") + private Long id; + + /** + * 键 + */ + @Schema(description = "键", example = "site_title") + @NotBlank(message = "键不能为空") + @Length(max = 100, message = "键长度不能超过 {max} 个字符") + private String code; + + /** + * 值 + */ + @Schema(description = "值", example = "Wms Admin") + private String value; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/OptionResetValueReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/OptionResetValueReq.java new file mode 100644 index 0000000..6f26137 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/OptionResetValueReq.java @@ -0,0 +1,34 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 参数重置请求参数 + * + * @author Bull-BCLS + * @since 2023/9/21 23:10 + */ +@Data +@Schema(description = "参数重置请求参数") +public class OptionResetValueReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 键列表 + */ + @Schema(description = "键列表", example = "SITE_TITLE,SITE_COPYRIGHT") + private List code; + + /** + * 类别 + */ + @Schema(description = "类别", example = "SITE") + private String category; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/PeopleEquipmentReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/PeopleEquipmentReq.java new file mode 100644 index 0000000..a0bc729 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/PeopleEquipmentReq.java @@ -0,0 +1,69 @@ +package top.wms.admin.system.model.req; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 创建或修改人员设备下发信息参数 + * + * @author zc + * @since 2025/03/27 14:59 + */ +@Data +@Schema(description = "创建或修改人员设备下发信息参数") +public class PeopleEquipmentReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Id + */ + @Schema(description = "Id") + private Long id; + + /** + * 人员Id + */ + @Schema(description = "人员Id") + private Long peopleId; + + /** + * 设备Id + */ + @Schema(description = "设备Id") + private Long equipmentId; + + /** + * 人员编号对接 + */ + @Schema(description = "人员编号对接") + @Length(max = 200, message = "人员编号对接长度不能超过 {max} 个字符") + private String guid; + + /** + * 人像对接 + */ + @Schema(description = "人像对接") + @Length(max = 200, message = "人像对接长度不能超过 {max} 个字符") + private String faceGuid; + + /** + * 访客Id + */ + @Schema(description = "访客Id") + private Long visitorId; + + /** + * 三方人员id + */ + @Schema(description = "三方人员id") + @Length(max = 36, message = "三方人员id长度不能超过 {max} 个字符") + private String otherId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/RoleReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/RoleReq.java new file mode 100644 index 0000000..83a1056 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/RoleReq.java @@ -0,0 +1,77 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.constant.RegexConstants; +import top.wms.admin.common.enums.DataScopeEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 创建或修改角色参数 + * + * @author Charles7c + * @since 2023/2/8 23:12 + */ +@Data +@Schema(description = "创建或修改角色参数") +public class RoleReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "测试人员") + @NotBlank(message = "名称不能为空") + @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "名称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "test") + @NotBlank(message = "编码不能为空") + @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") + private String code; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + @Min(value = 1, message = "排序最小值为 {value}") + private Integer sort; + + /** + * 描述 + */ + @Schema(description = "描述", example = "测试人员描述信息") + @Length(max = 200, message = "描述长度不能超过 {max} 个字符") + private String description; + + /** + * 数据权限 + */ + @Schema(description = "数据权限", example = "5") + private DataScopeEnum dataScope; + + /** + * 权限范围:部门 ID 列表 + */ + @Schema(description = "权限范围:部门 ID 列表", example = "5") + private List deptIds = new ArrayList<>(); + + /** + * 部门选择是否父子节点关联 + */ + @Schema(description = "部门选择是否父子节点关联", example = "false") + private Boolean deptCheckStrictly; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/RoleUpdatePermissionReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/RoleUpdatePermissionReq.java new file mode 100644 index 0000000..7fd80d4 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/RoleUpdatePermissionReq.java @@ -0,0 +1,41 @@ +package top.wms.admin.system.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 修改角色功能权限参数 + * + * @author Charles7c + * @since 2025/2/5 21:00 + */ +@Data +@Schema(description = "修改角色功能权限参数") +public class RoleUpdatePermissionReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 角色 ID + */ + @Schema(description = "角色 ID", example = "1") + private Long roleId; + + /** + * 功能权限:菜单 ID 列表 + */ + @Schema(description = "功能权限:菜单 ID 列表", example = "1000,1010,1011,1012,1013,1014") + private List menuIds = new ArrayList<>(); + + /** + * 菜单选择是否父子节点关联 + */ + @Schema(description = "菜单选择是否父子节点关联", example = "false") + private Boolean menuCheckStrictly; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/StorageReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/StorageReq.java new file mode 100644 index 0000000..7134a19 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/StorageReq.java @@ -0,0 +1,120 @@ +package top.wms.admin.system.model.req; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.constant.RegexConstants; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.StorageTypeEnum; +import top.wms.admin.system.validation.ValidationGroup; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 存储请求参数 + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +@Data +@Schema(description = "存储请求参数") +public class StorageReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "存储1") + @NotBlank(message = "名称不能为空") + @Length(max = 100, message = "名称长度不能超过 {max} 个字符") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "local") + @NotBlank(message = "编码不能为空") + @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") + private String code; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + @NotNull(message = "类型非法") + private StorageTypeEnum type; + + /** + * Access Key + */ + @Schema(description = "Access Key", example = "") + @Length(max = 255, message = "Access Key长度不能超过 {max} 个字符") + @NotBlank(message = "Access Key不能为空", groups = ValidationGroup.Storage.OSS.class) + private String accessKey; + + /** + * Secret Key + */ + @Schema(description = "Secret Key", example = "") + @NotBlank(message = "Secret Key不能为空", groups = ValidationGroup.Storage.OSS.class) + private String secretKey; + + /** + * Endpoint + */ + @Schema(description = "Endpoint", example = "") + @Length(max = 255, message = "Endpoint长度不能超过 {max} 个字符") + @NotBlank(message = "Endpoint不能为空", groups = ValidationGroup.Storage.OSS.class) + private String endpoint; + + /** + * Bucket/存储路径 + */ + @Schema(description = "Bucket/存储路径", example = "C:/wms-admin/data/file/") + @Length(max = 255, message = "Bucket长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.OSS.class) + @Length(max = 255, message = "存储路径长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.Local.class) + @NotBlank(message = "Bucket不能为空", groups = ValidationGroup.Storage.OSS.class) + @NotBlank(message = "存储路径不能为空", groups = ValidationGroup.Storage.Local.class) + private String bucketName; + + /** + * 域名/访问路径 + */ + @Schema(description = "域名/访问路径", example = "http://localhost:8000/file") + @Length(max = 255, message = "域名长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.OSS.class) + @Length(max = 255, message = "访问路径长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.Local.class) + @NotBlank(message = "访问路径不能为空", groups = ValidationGroup.Storage.Local.class) + private String domain; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + /** + * 描述 + */ + @Schema(description = "描述", example = "存储描述") + @Length(max = 200, message = "描述长度不能超过 {max} 个字符") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 是否为默认存储 + */ + @JsonIgnore + private Boolean isDefault; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserBasicInfoUpdateReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserBasicInfoUpdateReq.java new file mode 100644 index 0000000..5e55eac --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserBasicInfoUpdateReq.java @@ -0,0 +1,41 @@ +package top.wms.admin.system.model.req.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import top.wms.admin.common.constant.RegexConstants; +import top.wms.admin.common.enums.GenderEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户基础信息修改参数 + * + * @author Charles7c + * @since 2023/1/7 23:08 + */ +@Data +@Schema(description = "用户基础信息修改参数") +public class UserBasicInfoUpdateReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "张三") + @NotBlank(message = "用户名不能为空") + @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") + private String userName; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1") + @NotNull(message = "性别非法") + private GenderEnum gender; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserEmailUpdateRequest.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserEmailUpdateRequest.java new file mode 100644 index 0000000..b500618 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserEmailUpdateRequest.java @@ -0,0 +1,48 @@ +package top.wms.admin.system.model.req.user; + +import cn.hutool.core.lang.RegexPool; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户邮箱修改参数 + * + * @author Charles7c + * @since 2023/1/12 20:18 + */ +@Data +@Schema(description = "用户邮箱修改参数") +public class UserEmailUpdateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 新邮箱 + */ + @Schema(description = "新邮箱", example = "123456789@qq.com") + @NotBlank(message = "新邮箱不能为空") + @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") + private String email; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "888888") + @NotBlank(message = "验证码不能为空") + @Length(max = 6, message = "验证码非法") + private String captcha; + + /** + * 当前密码(加密) + */ + @Schema(description = "当前密码(加密)", example = "SYRLSszQGcMv4kP2Yolou9zf28B9GDakR9u91khxmR7V++i5A384kwnNZxqgvT6bjT4zqpIDuMFLWSt92hQJJA==") + @NotBlank(message = "当前密码不能为空") + private String oldPassword; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserImportReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserImportReq.java new file mode 100644 index 0000000..9ff0dc2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserImportReq.java @@ -0,0 +1,59 @@ +package top.wms.admin.system.model.req.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.ImportPolicyEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户导入参数 + * + * @author Kils + * @since 2024-6-17 16:42 + */ +@Data +@Schema(description = "用户导入参数") +public class UserImportReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 导入会话KEY + */ + @Schema(description = "导入会话KEY", example = "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed") + @NotBlank(message = "导入已过期,请重新上传") + private String importKey; + + /** + * 用户重复策略 + */ + @Schema(description = "重复用户策略", example = "1") + @NotNull(message = "重复用户策略不能为空") + private ImportPolicyEnum duplicateUser; + + /** + * 重复邮箱策略 + */ + @Schema(description = "重复邮箱策略", example = "1") + @NotNull(message = "重复邮箱策略不能为空") + private ImportPolicyEnum duplicateEmail; + + /** + * 重复手机策略 + */ + @Schema(description = "重复手机策略", example = "1") + @NotNull(message = "重复手机策略不能为空") + private ImportPolicyEnum duplicatePhone; + + /** + * 默认状态 + */ + @Schema(description = "默认状态", example = "1") + private DisEnableStatusEnum defaultStatus; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserImportRowReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserImportRowReq.java new file mode 100644 index 0000000..9f89f3f --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserImportRowReq.java @@ -0,0 +1,82 @@ +package top.wms.admin.system.model.req.user; + +import cn.hutool.core.lang.RegexPool; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.constant.RegexConstants; +import top.continew.starter.extension.crud.validation.CrudValidationGroup; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户导入行数据 + * + * @author Kils + * @since 2024-6-17 16:42 + */ +@Data +@Schema(description = "用户导入行数据") +public class UserImportRowReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头") + private String username; + + /** + * 昵称 + */ + @NotBlank(message = "昵称不能为空") + @Pattern(regexp = RegexConstants.GENERAL_NAME, message = "昵称长度为 2-30 个字符,支持中文、字母、数字、下划线,短横线") + private String nickname; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空", groups = CrudValidationGroup.Add.class) + private String password; + + /** + * 部门名称 + */ + @NotBlank(message = "所属部门不能为空") + private String deptName; + + /** + * 角色 + */ + @NotBlank(message = "所属角色不能为空") + private String roleName; + + /** + * 性别 + */ + private String gender; + + /** + * 邮箱 + */ + @Pattern(regexp = "^$|" + RegexPool.EMAIL, message = "邮箱格式错误") + @Length(max = 255, message = "邮箱长度不能超过 {max} 个字符") + private String email; + + /** + * 手机号码 + */ + @Pattern(regexp = "^$|" + RegexPool.MOBILE, message = "手机号码格式错误") + private String phone; + + /** + * 描述 + */ + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPasswordResetReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPasswordResetReq.java new file mode 100644 index 0000000..10e6837 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPasswordResetReq.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.model.req.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户密码重置参数 + * + * @author Charles7c + * @since 2024/2/2 22:50 + */ +@Data +@Schema(description = "用户密码重置参数") +public class UserPasswordResetReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 新密码(加密) + */ + @Schema(description = "新密码(加密)", example = "Gzc78825P5baH190lRuZFb9KJxRt/psN2jiyOMPoc5WRcCvneCwqDm3Q33BZY56EzyyVy7vQu7jQwYTK4j1+5w==") + @NotBlank(message = "新密码不能为空") + private String newPassword; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPasswordUpdateReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPasswordUpdateReq.java new file mode 100644 index 0000000..fe5b80e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPasswordUpdateReq.java @@ -0,0 +1,35 @@ +package top.wms.admin.system.model.req.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户密码修改参数 + * + * @author Charles7c + * @since 2023/1/9 23:28 + */ +@Data +@Schema(description = "用户密码修改参数") +public class UserPasswordUpdateReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 当前密码(加密) + */ + @Schema(description = "当前密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==") + private String oldPassword; + + /** + * 新密码(加密) + */ + @Schema(description = "新密码(加密)", example = "Gzc78825P5baH190lRuZFb9KJxRt/psN2jiyOMPoc5WRcCvneCwqDm3Q33BZY56EzyyVy7vQu7jQwYTK4j1+5w==") + @NotBlank(message = "新密码不能为空") + private String newPassword; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPhoneUpdateReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPhoneUpdateReq.java new file mode 100644 index 0000000..b534151 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserPhoneUpdateReq.java @@ -0,0 +1,48 @@ +package top.wms.admin.system.model.req.user; + +import cn.hutool.core.lang.RegexPool; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户手机号修改参数 + * + * @author Charles7c + * @since 2023/10/27 20:11 + */ +@Data +@Schema(description = "用户手机号修改参数") +public class UserPhoneUpdateReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 新手机号 + */ + @Schema(description = "新手机号", example = "13811111111") + @NotBlank(message = "新手机号不能为空") + @Pattern(regexp = RegexPool.MOBILE, message = "手机号格式错误") + private String phone; + + /** + * 验证码 + */ + @Schema(description = "验证码", example = "8888") + @NotBlank(message = "验证码不能为空") + @Length(max = 4, message = "验证码非法") + private String captcha; + + /** + * 当前密码(加密) + */ + @Schema(description = "当前密码(加密)", example = "SYRLSszQGcMv4kP2Yolou9zf28B9GDakR9u91khxmR7V++i5A384kwnNZxqgvT6bjT4zqpIDuMFLWSt92hQJJA==") + @NotBlank(message = "当前密码不能为空") + private String oldPassword; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserReq.java new file mode 100644 index 0000000..1934d20 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserReq.java @@ -0,0 +1,101 @@ +package top.wms.admin.system.model.req.user; + +import cn.hutool.core.lang.RegexPool; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.constant.RegexConstants; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.enums.GenderEnum; +import top.continew.starter.extension.crud.validation.CrudValidationGroup; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 创建或修改用户参数 + * + * @author Charles7c + * @since 2023/2/20 21:03 + */ +@Data +@Schema(description = "创建或修改用户参数") +public class UserReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + @NotBlank(message = "用户名不能为空") + @Pattern(regexp = RegexConstants.USERNAME, message = "用户名长度为 4-64 个字符,支持大小写字母、数字、下划线,以字母开头") + private String username; + + /** + * 密码(加密) + */ + @Schema(description = "密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==") + @NotBlank(message = "密码不能为空", groups = CrudValidationGroup.Add.class) + private String password; + + /* + * 卡号 + * */ + @Schema(description = "卡号") + private String cardNo; + + /** + * 邮箱 + */ + @Schema(description = "邮箱", example = "123456789@qq.com") + @Pattern(regexp = "^$|" + RegexPool.EMAIL, message = "邮箱格式错误") + @Length(max = 255, message = "邮箱长度不能超过 {max} 个字符") + private String email; + + /** + * 手机号码 + */ + @Schema(description = "手机号码", example = "13811111111") + @Pattern(regexp = "^$|" + RegexPool.MOBILE, message = "手机号码格式错误") + private String phone; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1") + @NotNull(message = "性别非法") + private GenderEnum gender; + + /** + * 所属角色 + */ + @Schema(description = "所属角色", example = "2") + @NotEmpty(message = "所属角色不能为空") + private List roleIds; + + /** + * 描述 + */ + @Schema(description = "描述", example = "张三描述信息") + @Length(max = 200, message = "描述长度不能超过 {max} 个字符") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 设备ID + */ + @Schema(description = "设备ID") + private Long equipmentId; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserRoleUpdateReq.java b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserRoleUpdateReq.java new file mode 100644 index 0000000..e3e508f --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/req/user/UserRoleUpdateReq.java @@ -0,0 +1,30 @@ +package top.wms.admin.system.model.req.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 用户角色修改参数 + * + * @author Charles7c + * @since 2023/2/24 23:05 + */ +@Data +@Schema(description = "用户角色修改参数") +public class UserRoleUpdateReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 角色 ID 列表 + */ + @Schema(description = "所属角色", example = "1,2") + @NotEmpty(message = "所属角色不能为空") + private List roleIds; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/AvatarResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/AvatarResp.java new file mode 100644 index 0000000..93e540f --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/AvatarResp.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 头像信息 + * + * @author Charles7c + * @since 2023/1/2 16:29 + */ +@Data +@Builder +@Schema(description = "头像信息") +public class AvatarResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 头像地址 + */ + @Schema(description = "头像地址", example = "https://himg.bdimg.com/sys/portrait/item/public.1.81ac9a9e.rf1ix17UfughLQjNo7XQ_w.jpg") + private String avatar; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ClientResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ClientResp.java new file mode 100644 index 0000000..32600f8 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ClientResp.java @@ -0,0 +1,72 @@ +package top.wms.admin.system.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.enums.DisEnableStatusEnum; + +import java.io.Serial; +import java.util.List; + +/** + * 终端信息 + * + * @author KAI + * @author Charles7c + * @since 2024/12/03 16:04 + */ +@Data +@Schema(description = "终端信息") +public class ClientResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 终端 ID + */ + @Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a") + private String clientId; + + /** + * 终端 Key + */ + @Schema(description = "终端 Key", example = "PC") + private String clientKey; + + /** + * 终端秘钥 + */ + @Schema(description = "终端秘钥", example = "dd77ab1e353a027e0d60ce3b151e8642") + private String clientSecret; + + /** + * 认证类型 + */ + @Schema(description = "认证类型", example = "ACCOUNT") + private List authType; + + /** + * 终端类型 + */ + @Schema(description = "终端类型", example = "PC") + private String clientType; + + /** + * Token 最低活跃频率(单位:秒,-1:不限制,永不冻结) + */ + @Schema(description = "Token 最低活跃频率(单位:秒,-1:不限制,永不冻结)", example = "1800") + private Long activeTimeout; + + /** + * Token 有效期(单位:秒,-1:永不过期) + */ + @Schema(description = "Token 有效期(单位:秒,-1:永不过期)", example = "86400") + private Long timeout; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ConfigDetailResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ConfigDetailResp.java new file mode 100644 index 0000000..82b171a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ConfigDetailResp.java @@ -0,0 +1,56 @@ +package top.wms.admin.system.model.resp; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; + +import top.wms.admin.common.model.resp.BaseDetailResp; + +import java.io.Serial; +import java.time.*; + +/** + * 参数配置详情信息 + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "参数配置详情信息") +public class ConfigDetailResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 参数名称 + */ + @Schema(description = "参数名称") + @ExcelProperty(value = "参数名称") + private String configName; + + /** + * 参数键名 + */ + @Schema(description = "参数键名") + @ExcelProperty(value = "参数键名") + private String configKey; + + /** + * 参数键值 + */ + @Schema(description = "参数键值") + @ExcelProperty(value = "参数键值") + private String configValue; + + /** + * 备注 + */ + @Schema(description = "备注") + @ExcelProperty(value = "备注") + private String remark; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ConfigResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ConfigResp.java new file mode 100644 index 0000000..98a2e99 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/ConfigResp.java @@ -0,0 +1,48 @@ +package top.wms.admin.system.model.resp; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import top.wms.admin.common.model.resp.BaseDetailResp; + +import java.io.Serial; +import java.time.*; + +/** + * 参数配置信息 + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Data +@Schema(description = "参数配置信息") +public class ConfigResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 参数名称 + */ + @Schema(description = "参数名称") + private String configName; + + /** + * 参数键名 + */ + @Schema(description = "参数键名") + private String configKey; + + /** + * 参数键值 + */ + @Schema(description = "参数键值") + private String configValue; + + /** + * 备注 + */ + @Schema(description = "备注") + private String remark; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DeptResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DeptResp.java new file mode 100644 index 0000000..e82cb9a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DeptResp.java @@ -0,0 +1,70 @@ +package top.wms.admin.system.model.resp; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.continew.starter.extension.crud.annotation.TreeField; +import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; + +import java.io.Serial; + +/** + * 部门信息 + * + * @author Charles7c + * @since 2023/1/22 13:53 + */ +@Data +@ExcelIgnoreUnannotated +@TreeField(value = "id", nameKey = "name") +@Schema(description = "部门信息") +public class DeptResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "测试部") + @ExcelProperty(value = "名称", order = 2) + private String name; + + /** + * 上级部门 ID + */ + @Schema(description = "上级部门 ID", example = "2") + @ExcelProperty(value = "上级部门 ID", order = 3) + private Long parentId; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 5) + private DisEnableStatusEnum status; + + /** + * 排序 + */ + @Schema(description = "排序", example = "3") + @ExcelProperty(value = "排序", order = 6) + private Integer sort; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "是否为系统内置数据", example = "false") + @ExcelProperty(value = "系统内置", order = 7) + private Boolean isSystem; + + /** + * 描述 + */ + @Schema(description = "描述", example = "测试部描述信息") + @ExcelProperty(value = "描述", order = 8) + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DictItemResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DictItemResp.java new file mode 100644 index 0000000..653bd1c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DictItemResp.java @@ -0,0 +1,67 @@ +package top.wms.admin.system.model.resp; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; + +import java.io.Serial; + +/** + * 字典项信息 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@Schema(description = "字典项信息") +public class DictItemResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标签 + */ + @Schema(description = "标签", example = "通知") + private String label; + + /** + * 值 + */ + @Schema(description = "值", example = "1") + private String value; + + /** + * 标签颜色 + */ + @Schema(description = "标签颜色", example = "blue") + private String color; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class) + private DisEnableStatusEnum status; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + /** + * 描述 + */ + @Schema(description = "描述", example = "通知描述信息") + private String description; + + /** + * 字典 ID + */ + @Schema(description = "字典 ID", example = "1") + private Long dictId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DictResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DictResp.java new file mode 100644 index 0000000..3049cbf --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/DictResp.java @@ -0,0 +1,45 @@ +package top.wms.admin.system.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; + +import java.io.Serial; + +/** + * 字典信息 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Data +@Schema(description = "字典信息") +public class DictResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "公告类型") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "notice_type") + private String code; + + /** + * 描述 + */ + @Schema(description = "描述", example = "公告类型描述信息") + private String description; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "是否为系统内置数据", example = "true") + private Boolean isSystem; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/MenuResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/MenuResp.java new file mode 100644 index 0000000..277d16a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/MenuResp.java @@ -0,0 +1,109 @@ +package top.wms.admin.system.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseResp; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.MenuTypeEnum; +import top.continew.starter.extension.crud.annotation.TreeField; + +import java.io.Serial; + +/** + * 菜单信息 + * + * @author Charles7c + * @since 2023/2/15 20:23 + */ +@Data +@TreeField(value = "id") +@Schema(description = "菜单信息") +public class MenuResp extends BaseResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标题 + */ + @Schema(description = "标题", example = "用户管理") + private String title; + + /** + * 上级菜单 ID + */ + @Schema(description = "上级菜单 ID", example = "1000") + private Long parentId; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + private MenuTypeEnum type; + + /** + * 路由地址 + */ + @Schema(description = "路由地址", example = "/system/user") + private String path; + + /** + * 组件名称 + */ + @Schema(description = "组件名称", example = "User") + private String name; + + /** + * 组件路径 + */ + @Schema(description = "组件路径", example = "/system/user/index") + private String component; + + /** + * 重定向地址 + */ + @Schema(description = "重定向地址") + private String redirect; + + /** + * 图标 + */ + @Schema(description = "图标", example = "user") + private String icon; + + /** + * 是否外链 + */ + @Schema(description = "是否外链", example = "false") + private Boolean isExternal; + + /** + * 是否缓存 + */ + @Schema(description = "是否缓存", example = "false") + private Boolean isCache; + + /** + * 是否隐藏 + */ + @Schema(description = "是否隐藏", example = "false") + private Boolean isHidden; + + /** + * 权限标识 + */ + @Schema(description = "权限标识", example = "system:user:list") + private String permission; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/OptionResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/OptionResp.java new file mode 100644 index 0000000..1ae26d6 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/OptionResp.java @@ -0,0 +1,63 @@ +package top.wms.admin.system.model.resp; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 参数信息 + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +@Data +@Schema(description = "参数信息") +public class OptionResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 名称 + */ + @Schema(description = "名称", example = "系统标题") + private String name; + + /** + * 键 + */ + @Schema(description = "键", example = "site_title") + private String code; + + /** + * 值 + */ + @Schema(description = "值", example = "Wms Admin") + private String value; + + /** + * 默认值 + */ + @JsonIgnore + private String defaultValue; + + /** + * 描述 + */ + @Schema(description = "描述", example = "用于显示登录页面的系统标题。") + private String description; + + public String getValue() { + return StrUtil.nullToDefault(value, defaultValue); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/PeopleEquipmentResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/PeopleEquipmentResp.java new file mode 100644 index 0000000..41d8fd2 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/PeopleEquipmentResp.java @@ -0,0 +1,59 @@ +package top.wms.admin.system.model.resp; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import top.wms.admin.common.model.resp.BaseDetailResp; + +import java.io.Serial; + +/** + * 人员设备下发信息信息 + * + * @author zc + * @since 2025/03/27 14:59 + */ +@Data +@Schema(description = "人员设备下发信息信息") +public class PeopleEquipmentResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 人员Id + */ + @Schema(description = "人员Id") + private Long peopleId; + + /** + * 设备Id + */ + @Schema(description = "设备Id") + private Long equipmentId; + + /** + * 人员编号对接 + */ + @Schema(description = "人员编号对接") + private String guid; + + /** + * 人像对接 + */ + @Schema(description = "人像对接") + private String faceGuid; + + /** + * 访客Id + */ + @Schema(description = "访客Id") + private Long visitorId; + + /** + * 三方人员id + */ + @Schema(description = "三方人员id") + private String otherId; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/StorageResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/StorageResp.java new file mode 100644 index 0000000..a1a1fb9 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/StorageResp.java @@ -0,0 +1,103 @@ +package top.wms.admin.system.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.StorageTypeEnum; +import top.continew.starter.security.mask.annotation.JsonMask; + +import java.io.Serial; + +/** + * 存储响应信息 + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +@Data +@Schema(description = "存储响应信息") +public class StorageResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "存储1") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "local") + private String code; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + private StorageTypeEnum type; + + /** + * Access Key + */ + @Schema(description = "Access Key", example = "") + private String accessKey; + + /** + * Secret Key + */ + @Schema(description = "Secret Key", example = "") + @JsonMask(left = 4, right = 3) + private String secretKey; + + /** + * Endpoint + */ + @Schema(description = "Endpoint", example = "") + private String endpoint; + + /** + * Bucket/存储路径 + */ + @Schema(description = "Bucket/存储路径", example = "C:/wms-admin/data/file/") + private String bucketName; + + /** + * 域名/访问路径 + */ + @Schema(description = "域名", example = "http://localhost:8000/file") + private String domain; + + /** + * 描述 + */ + @Schema(description = "描述", example = "存储描述") + private String description; + + /** + * 是否为默认存储 + */ + @Schema(description = "是否为默认存储", example = "true") + private Boolean isDefault; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + @Override + public Boolean getDisabled() { + return this.getIsDefault(); + } + +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileResp.java new file mode 100644 index 0000000..a2b5bc4 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileResp.java @@ -0,0 +1,114 @@ +package top.wms.admin.system.model.resp.file; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.system.enums.FileTypeEnum; + +import java.io.Serial; + +/** + * 文件信息 + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "文件详情信息") +public class FileResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "example") + private String name; + + /** + * 大小(字节) + */ + @Schema(description = "大小(字节)", example = "4096") + private Long size; + + /** + * URL + */ + @Schema(description = "URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/example/example.jpg") + private String url; + + /** + * 上级目录 + */ + @Schema(description = "上级目录", example = "25") + private String parentPath; + + /** + * 绝对路径 + */ + @Schema(description = "绝对路径", example = "/2025/2/25") + private String absPath; + + /** + * 扩展名 + */ + @Schema(description = "扩展名", example = "jpg") + private String extension; + + /** + * 内容类型 + */ + @Schema(description = "内容类型", example = "image/jpeg") + private String contentType; + + /** + * 类型 + */ + @Schema(description = "类型", example = "2") + private FileTypeEnum type; + + /** + * MD5 值 + */ + @Schema(description = "MD5值", example = "193572f83684128a0d0f993e97100f8a") + private String md5; + + /** + * 元数据 + */ + @Schema(description = "元数据", example = "{width:1024,height:1024}") + private String metadata; + + /** + * 缩略图大小(字节) + */ + @Schema(description = "缩略图大小(字节)", example = "1024") + private Long thumbnailSize; + + /** + * 缩略图 URL + */ + @Schema(description = "缩略图 URL", example = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/example/example.jpg.min.jpg") + private String thumbnailUrl; + + /** + * 缩略图元数据 + */ + @Schema(description = "缩略图文件元数据", example = "{width:100,height:100}") + private String thumbnailMetadata; + + /** + * 存储 ID + */ + @Schema(description = "存储 ID", example = "1") + private Long storageId; + + /** + * 存储名称 + */ + @Schema(description = "存储名称", example = "MinIO") + private String storageName; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileStatisticsResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileStatisticsResp.java new file mode 100644 index 0000000..927feb8 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileStatisticsResp.java @@ -0,0 +1,49 @@ +package top.wms.admin.system.model.resp.file; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.system.enums.FileTypeEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 文件资源统计信息 + * + * @author Kils + * @since 2024/4/30 14:30 + */ +@Data +@Schema(description = "文件资源统计信息") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileStatisticsResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 文件类型 + */ + @Schema(description = "类型", example = "2") + private FileTypeEnum type; + + /** + * 大小(字节) + */ + @Schema(description = "大小(字节)", example = "4096") + private Long size; + + /** + * 数量 + */ + @Schema(description = "数量", example = "1000") + private Long number; + + /** + * 分类数据 + */ + @Schema(description = "分类数据") + private List data; +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileUploadResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileUploadResp.java new file mode 100644 index 0000000..f9944b3 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/file/FileUploadResp.java @@ -0,0 +1,48 @@ +package top.wms.admin.system.model.resp.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 文件上传响应信息 + * + * @author Charles7c + * @since 2024/3/6 22:26 + */ +@Data +@Builder +@Schema(description = "文件上传响应信息") +public class FileUploadResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 文件 id + */ + @Schema(description = "文件 id", example = "1897293810343682049") + private String id; + + /** + * 文件 URL + */ + @Schema(description = "文件 URL", example = "http://localhost:8000/file/65e87dc3fb377a6fb58bdece.jpg") + private String url; + + /** + * 缩略图文件 URL + */ + @Schema(description = "缩略图文件 URL", example = "http://localhost:8000/file/65e87dc3fb377a6fb58bdece.jpg") + private String thUrl; + + /** + * 元数据 + */ + @Schema(description = "元数据") + private Map metadata; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LogDetailResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LogDetailResp.java new file mode 100644 index 0000000..a3851b9 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LogDetailResp.java @@ -0,0 +1,155 @@ +package top.wms.admin.system.model.resp.log; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.annotation.condition.ConditionOnPropertyNotNull; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.system.enums.LogStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 日志详情信息 + * + * @author Charles7c + * @since 2023/1/18 20:19 + */ +@Data +@Schema(description = "日志详情信息") +public class LogDetailResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 链路 ID + */ + @Schema(description = "链路 ID", example = "904846526308876288") + private String traceId; + + /** + * 日志描述 + */ + @Schema(description = "日志描述", example = "新增数据") + private String description; + + /** + * 所属模块 + */ + @Schema(description = "所属模块", example = "部门管理") + private String module; + + /** + * 请求 URL + */ + @Schema(description = "请求 URL", example = "http://api.wms.top/system/dept") + private String requestUrl; + + /** + * 请求方式 + */ + @Schema(description = "请求方式", example = "POST") + private String requestMethod; + + /** + * 请求头 + */ + @Schema(description = "请求头", example = "{\"Origin\": [\"https://admin.wms.top\"],...}") + private String requestHeaders; + + /** + * 请求体 + */ + @Schema(description = "请求体", example = "{\"name\": \"测试部\",...}") + private String requestBody; + + /** + * 状态码 + */ + @Schema(description = "状态码", example = "200") + private Integer statusCode; + + /** + * 响应头 + */ + @Schema(description = "响应头", example = "{\"Content-Type\": [\"application/json\"],...}") + private String responseHeaders; + + /** + * 响应体 + */ + @Schema(description = "响应体", example = "{\"success\":true},...") + private String responseBody; + + /** + * 耗时(ms) + */ + @Schema(description = "耗时(ms)", example = "58") + private Long timeTaken; + + /** + * IP + */ + @Schema(description = "IP", example = "") + private String ip; + + /** + * IP 归属地 + */ + @Schema(description = "IP 归属地", example = "中国北京北京市") + private String address; + + /** + * 浏览器 + */ + @Schema(description = "浏览器", example = "Chrome 115.0.0.0") + private String browser; + + /** + * 操作系统 + */ + @Schema(description = "操作系统", example = "Windows 10") + private String os; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private LogStatusEnum status; + + /** + * 错误信息 + */ + @Schema(description = "错误信息") + private String errorMsg; + + /** + * 创建人 + */ + @JsonIgnore + @ConditionOnPropertyNotNull + @Assemble(prop = ":createUserString", container = ContainerConstants.USER_NICKNAME) + private Long createUser; + + /** + * 创建人 + */ + @Schema(description = "创建人", example = "张三") + private String createUserString; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime createTime; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LogResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LogResp.java new file mode 100644 index 0000000..69df61a --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LogResp.java @@ -0,0 +1,102 @@ +package top.wms.admin.system.model.resp.log; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.system.enums.LogStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 日志信息 + * + * @author Charles7c + * @since 2023/1/14 18:27 + */ +@Data +@Schema(description = "日志信息") +public class LogResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 日志描述 + */ + @Schema(description = "日志描述", example = "新增数据") + private String description; + + /** + * 所属模块 + */ + @Schema(description = "所属模块", example = "部门管理") + private String module; + + /** + * 耗时(ms) + */ + @Schema(description = "耗时(ms)", example = "58") + private Long timeTaken; + + /** + * IP + */ + @Schema(description = "IP", example = "") + private String ip; + + /** + * IP 归属地 + */ + @Schema(description = "IP 归属地", example = "中国北京北京市") + private String address; + + /** + * 浏览器 + */ + @Schema(description = "浏览器", example = "Chrome 115.0.0.0") + private String browser; + + /** + * 操作系统 + */ + @Schema(description = "操作系统", example = "Windows 10") + private String os; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private LogStatusEnum status; + + /** + * 错误信息 + */ + @Schema(description = "错误信息") + private String errorMsg; + + /** + * 创建人 + */ + @JsonIgnore + private Long createUser; + + /** + * 创建人 + */ + @Schema(description = "创建人", example = "张三") + private String createUserString; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime createTime; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LoginLogExportResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LoginLogExportResp.java new file mode 100644 index 0000000..7c36732 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/LoginLogExportResp.java @@ -0,0 +1,90 @@ +package top.wms.admin.system.model.resp.log; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.system.enums.LogStatusEnum; +import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 登录日志导出信息 + * + * @author Charles7c + * @since 2023/1/14 18:27 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "登录日志导出信息") +public class LoginLogExportResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + @ExcelProperty(value = "ID") + private Long id; + + /** + * 登录时间 + */ + @Schema(description = "登录时间", example = "2023-08-08 08:08:08", type = "string") + @ExcelProperty(value = "登录时间") + private LocalDateTime createTime; + + /** + * 用户昵称 + */ + @Schema(description = "用户昵称", example = "张三") + @ExcelProperty(value = "用户昵称") + private String createUserString; + + /** + * 登录行为 + */ + @Schema(description = "登录行为", example = "账号登录") + @ExcelProperty(value = "登录行为") + private String description; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class) + private LogStatusEnum status; + + /** + * 登录 IP + */ + @Schema(description = "登录 IP", example = "") + @ExcelProperty(value = "登录 IP") + private String ip; + + /** + * 登录地点 + */ + @Schema(description = "登录地点", example = "中国北京北京市") + @ExcelProperty(value = "登录地点") + private String address; + + /** + * 浏览器 + */ + @Schema(description = "浏览器", example = "Chrome 115.0.0.0") + @ExcelProperty(value = "浏览器") + private String browser; + + /** + * 终端系统 + */ + @Schema(description = "终端系统", example = "Windows 10") + @ExcelProperty(value = "终端系统") + private String os; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/OperationLogExportResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/OperationLogExportResp.java new file mode 100644 index 0000000..946bade --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/log/OperationLogExportResp.java @@ -0,0 +1,104 @@ +package top.wms.admin.system.model.resp.log; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.system.enums.LogStatusEnum; +import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 操作日志导出信息 + * + * @author Charles7c + * @since 2023/1/14 18:27 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "操作日志导出信息") +public class OperationLogExportResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + @ExcelProperty(value = "ID") + private Long id; + + /** + * 操作时间 + */ + @Schema(description = "操作时间", example = "2023-08-08 08:08:08", type = "string") + @ExcelProperty(value = "操作时间") + private LocalDateTime createTime; + + /** + * 操作人 + */ + @Schema(description = "操作人", example = "张三") + @ExcelProperty(value = "操作人") + private String createUserString; + + /** + * 操作内容 + */ + @Schema(description = "操作内容", example = "账号登录") + @ExcelProperty(value = "操作内容") + private String description; + + /** + * 所属模块 + */ + @Schema(description = "所属模块", example = "部门管理") + @ExcelProperty(value = "所属模块") + private String module; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class) + private LogStatusEnum status; + + /** + * 操作 IP + */ + @Schema(description = "操作 IP", example = "") + @ExcelProperty(value = "操作 IP") + private String ip; + + /** + * 操作地点 + */ + @Schema(description = "操作地点", example = "中国北京北京市") + @ExcelProperty(value = "操作地点") + private String address; + + /** + * 耗时(ms) + */ + @Schema(description = "耗时(ms)", example = "58") + @ExcelProperty(value = "耗时(ms)") + private Long timeTaken; + + /** + * 浏览器 + */ + @Schema(description = "浏览器", example = "Chrome 115.0.0.0") + @ExcelProperty(value = "浏览器") + private String browser; + + /** + * 终端系统 + */ + @Schema(description = "终端系统", example = "Windows 10") + @ExcelProperty(value = "终端系统") + private String os; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleDetailResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleDetailResp.java new file mode 100644 index 0000000..289f373 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleDetailResp.java @@ -0,0 +1,103 @@ +package top.wms.admin.system.model.resp.role; + +import cn.crane4j.annotation.AssembleMethod; +import cn.crane4j.annotation.ContainerMethod; +import cn.crane4j.annotation.MappingType; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.enums.DataScopeEnum; +import top.wms.admin.system.service.RoleDeptService; +import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; + +import java.io.Serial; +import java.util.List; + +/** + * 角色详情信息 + * + * @author Charles7c + * @since 2023/2/1 22:19 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "角色详情信息") +@AssembleMethod(key = "id", prop = ":deptIds", targetType = RoleDeptService.class, method = @ContainerMethod(bindMethod = "listDeptIdByRoleId", type = MappingType.ORDER_OF_KEYS)) +public class RoleDetailResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "测试人员") + @ExcelProperty(value = "名称") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "test") + @ExcelProperty(value = "编码") + private String code; + + /** + * 数据权限 + */ + @Schema(description = "数据权限", example = "5") + @ExcelProperty(value = "数据权限", converter = ExcelBaseEnumConverter.class) + private DataScopeEnum dataScope; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + @ExcelProperty(value = "排序") + private Integer sort; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "是否为系统内置数据", example = "false") + @ExcelProperty(value = "系统内置") + private Boolean isSystem; + + /** + * 菜单选择是否父子节点关联 + */ + @Schema(description = "菜单选择是否父子节点关联", example = "false") + private Boolean menuCheckStrictly; + + /** + * 部门选择是否父子节点关联 + */ + @Schema(description = "部门选择是否父子节点关联", example = "false") + private Boolean deptCheckStrictly; + + /** + * 描述 + */ + @Schema(description = "描述", example = "测试人员描述信息") + @ExcelProperty(value = "描述") + private String description; + + /** + * 功能权限:菜单 ID 列表 + */ + @Schema(description = "功能权限:菜单 ID 列表", example = "1000,1010,1011,1012,1013,1014") + private List menuIds; + + /** + * 权限范围:部门 ID 列表 + */ + @Schema(description = "权限范围:部门 ID 列表", example = "5") + private List deptIds; + + @Override + public Boolean getDisabled() { + return this.getIsSystem(); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleResp.java new file mode 100644 index 0000000..5eea37b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleResp.java @@ -0,0 +1,58 @@ +package top.wms.admin.system.model.resp.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.enums.DataScopeEnum; + +import java.io.Serial; + +/** + * 角色信息 + * + * @author Charles7c + * @since 2023/2/8 23:05 + */ +@Data +@Schema(description = "角色信息") +public class RoleResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 名称 + */ + @Schema(description = "名称", example = "测试人员") + private String name; + + /** + * 编码 + */ + @Schema(description = "编码", example = "test") + private String code; + + /** + * 数据权限 + */ + @Schema(description = "数据权限", example = "5") + private DataScopeEnum dataScope; + + /** + * 排序 + */ + @Schema(description = "排序", example = "1") + private Integer sort; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "是否为系统内置数据", example = "false") + private Boolean isSystem; + + /** + * 描述 + */ + @Schema(description = "描述", example = "测试人员描述信息") + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleUserResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleUserResp.java new file mode 100644 index 0000000..55abebb --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/role/RoleUserResp.java @@ -0,0 +1,113 @@ +package top.wms.admin.system.model.resp.role; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.core.executor.handler.ManyToManyAssembleOperationHandler; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.enums.GenderEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * 角色关联用户信息 + * + * @author Charles7c + * @since 2025/2/5 22:01 + */ +@Data +@Schema(description = "角色关联用户信息") +public class RoleUserResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 角色 ID + */ + @Schema(description = "角色 ID", example = "1") + private Long roleId; + + /** + * 用户 ID + */ + @Schema(description = "用户 ID", example = "1") + @Assemble(prop = ":roleIds", sort = 0, container = ContainerConstants.USER_ROLE_ID_LIST) + private Long userId; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + private String username; + + /** + * 昵称 + */ + @Schema(description = "昵称", example = "张三") + private String nickname; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1") + private GenderEnum gender; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "是否为系统内置数据", example = "false") + private Boolean isSystem; + + /** + * 描述 + */ + @Schema(description = "描述", example = "测试人员描述信息") + private String description; + + /** + * 部门 ID + */ + @Schema(description = "部门 ID", example = "5") + private Long deptId; + + /** + * 所属部门 + */ + @Schema(description = "所属部门", example = "测试部") + private String deptName; + + /** + * 角色 ID 列表 + */ + @Schema(description = "角色 ID 列表", example = "2") + @Assemble(prop = ":roleNames", container = ContainerConstants.USER_ROLE_NAME_LIST, handlerType = ManyToManyAssembleOperationHandler.class) + private List roleIds; + + /** + * 角色名称列表 + */ + @Schema(description = "角色名称列表", example = "测试人员") + private List roleNames; + + public Boolean getDisabled() { + return this.getIsSystem() && Objects.equals(roleId, SysConstants.SUPER_ROLE_ID); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserDetailResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserDetailResp.java new file mode 100644 index 0000000..06ef87b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserDetailResp.java @@ -0,0 +1,123 @@ +package top.wms.admin.system.model.resp.user; + +import cn.crane4j.annotation.Assemble; +import cn.crane4j.core.executor.handler.ManyToManyAssembleOperationHandler; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.enums.GenderEnum; +import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; +import top.continew.starter.file.excel.converter.ExcelListConverter; +import top.continew.starter.security.crypto.annotation.FieldEncrypt; + +import java.io.Serial; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +/** + * 用户详情信息 + * + * @author Charles7c + * @since 2023/2/20 21:11 + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "用户详情信息") +@Assemble(key = "id", prop = ":roleIds", sort = 0, container = ContainerConstants.USER_ROLE_ID_LIST) +public class UserDetailResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + @ExcelProperty(value = "用户名", order = 2) + private String username; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 4) + private DisEnableStatusEnum status; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1") + @ExcelProperty(value = "性别", converter = ExcelBaseEnumConverter.class, order = 5) + private GenderEnum gender; + + /** + * 角色 ID 列表 + */ + @Schema(description = "角色 ID 列表", example = "2") + @Assemble(prop = ":roleNames", container = ContainerConstants.USER_ROLE_NAME_LIST, handlerType = ManyToManyAssembleOperationHandler.class) + @ExcelProperty(value = "角色 ID 列表", converter = ExcelListConverter.class, order = 8) + private List roleIds; + + /** + * 角色名称列表 + */ + @Schema(description = "角色名称列表", example = "测试人员") + @ExcelProperty(value = "角色", converter = ExcelListConverter.class, order = 9) + private List roleNames; + + /** + * 手机号码 + */ + @Schema(description = "手机号码", example = "13811111111") + @ExcelProperty(value = "手机号码", order = 10) + @FieldEncrypt + private String phone; + + @Schema(description = "卡号") + @ExcelProperty(value = "卡号", order = 11) + private String cardNo; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "系统内置", example = "false") + @ExcelProperty(value = "系统内置", order = 13) + private Boolean isSystem; + + /** + * 描述 + */ + @Schema(description = "描述", example = "张三描述信息") + @ExcelProperty(value = "描述", order = 14) + private String description; + + /** + * 头像地址 + */ + @Schema(description = "头像地址", example = "https://himg.bdimg.com/sys/portrait/item/public.1.81ac9a9e.rf1ix17UfughLQjNo7XQ_w.jpg") + @ExcelProperty(value = "头像地址", order = 15) + private String avatar; + + /** + * 最后一次修改密码时间 + */ + @Schema(description = "最后一次修改密码时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime pwdResetTime; + + /** + * 设备ID + */ + @Schema(description = "设备ID") + private Long equipmentId; + + @Override + public Boolean getDisabled() { + return this.getIsSystem() || Objects.equals(this.getId(), UserContextHolder.getUserId()); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserImportParseResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserImportParseResp.java new file mode 100644 index 0000000..2680818 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserImportParseResp.java @@ -0,0 +1,61 @@ +package top.wms.admin.system.model.resp.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户导入解析结果 + * + * @author kils + * @since 2024/6/18 14:37 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "用户导入解析结果") +public class UserImportParseResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 导入会话 Key + */ + @Schema(description = "导入会话Key", example = "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed") + private String importKey; + + /** + * 总计行数 + */ + @Schema(description = "总计行数", example = "100") + private Integer totalRows; + + /** + * 有效行数 + */ + @Schema(description = "有效行数", example = "100") + private Integer validRows; + + /** + * 重复行数 + */ + @Schema(description = "重复行数", example = "100") + private Integer duplicateUserRows; + + /** + * 重复邮箱行数 + */ + @Schema(description = "重复邮箱行数", example = "100") + private Integer duplicateEmailRows; + + /** + * 重复手机行数 + */ + @Schema(description = "重复手机行数", example = "100") + private Integer duplicatePhoneRows; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserImportResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserImportResp.java new file mode 100644 index 0000000..507e140 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserImportResp.java @@ -0,0 +1,39 @@ +package top.wms.admin.system.model.resp.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户导入结果 + * + * @author kils + * @since 2024-06-18 14:37 + */ +@Data +@Schema(description = "用户导入结果") +@AllArgsConstructor +@NoArgsConstructor +public class UserImportResp { + + private static final long serialVersionUID = 1L; + + /** + * 总计行数 + */ + @Schema(description = "总计行数", example = "100") + private Integer totalRows; + + /** + * 新增行数 + */ + @Schema(description = "新增行数", example = "100") + private Integer insertRows; + + /** + * 修改行数 + */ + @Schema(description = "修改行数", example = "100") + private Integer updateRows; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserResp.java new file mode 100644 index 0000000..71a1287 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserResp.java @@ -0,0 +1,94 @@ +package top.wms.admin.system.model.resp.user; + +import cn.crane4j.annotation.Assemble; +import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.common.model.resp.BaseDetailResp; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.enums.GenderEnum; +import top.continew.starter.security.mask.annotation.JsonMask; +import top.continew.starter.security.mask.enums.MaskType; + +import java.io.Serial; +import java.util.List; +import java.util.Objects; + +/** + * 用户信息 + * + * @author Charles7c + * @since 2023/2/20 21:08 + */ +@Data +@Schema(description = "用户信息") +@Assemble(key = "id", prop = ":roleIds", sort = 0, container = ContainerConstants.USER_ROLE_ID_LIST) +public class UserResp extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @Schema(description = "用户名", example = "zhangsan") + @ExcelProperty(value = "用户名") + private String username; + + @Schema(description = "卡号") + @ExcelProperty(value = "卡号") + private String cardNo; + + /** + * 性别 + */ + @Schema(description = "性别", example = "1") + private GenderEnum gender; + + /** + * 头像地址 + */ + @Schema(description = "头像地址", example = "https://himg.bdimg.com/sys/portrait/item/public.1.81ac9a9e.rf1ix17UfughLQjNo7XQ_w.jpg") + @ExcelIgnore + private String avatar; + + /** + * 手机号码 + */ + @Schema(description = "手机号码", example = "188****8888") + @JsonMask(MaskType.MOBILE_PHONE) + private String phone; + + /** + * 状态 + */ + @Schema(description = "状态", example = "1") + private DisEnableStatusEnum status; + + /** + * 是否为系统内置数据 + */ + @Schema(description = "是否为系统内置数据", example = "false") + @ExcelIgnore + private Boolean isSystem; + + /** + * 描述 + */ + @Schema(description = "描述", example = "张三描述信息") + private String description; + + /** + * 角色名称列表 + */ + @Schema(description = "角色名称列表", example = "测试人员") + private List roleNames; + + @Override + public Boolean getDisabled() { + return this.getIsSystem() || Objects.equals(this.getId(), UserContextHolder.getUserId()); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserSocialBindResp.java b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserSocialBindResp.java new file mode 100644 index 0000000..6e3b33e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/model/resp/user/UserSocialBindResp.java @@ -0,0 +1,33 @@ +package top.wms.admin.system.model.resp.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 第三方账号绑定信息 + * + * @author Charles7c + * @since 2023/10/19 21:29 + */ +@Data +@Schema(description = "第三方账号绑定信息") +public class UserSocialBindResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 来源 + */ + @Schema(description = "来源", example = "GITEE") + private String source; + + /** + * 描述 + */ + @Schema(description = "描述", example = "码云") + private String description; +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/ClientService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/ClientService.java new file mode 100644 index 0000000..8e7e0c9 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/ClientService.java @@ -0,0 +1,24 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.model.query.ClientQuery; +import top.wms.admin.system.model.req.ClientReq; +import top.wms.admin.system.model.resp.ClientResp; +import top.continew.starter.extension.crud.service.BaseService; + +/** + * 终端业务接口 + * + * @author KAI + * @author Charles7c + * @since 2024/12/03 16:04 + */ +public interface ClientService extends BaseService { + + /** + * 根据终端 ID 查詢 + * + * @param clientId 终端 ID + * @return 终端信息 + */ + ClientResp getByClientId(String clientId); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/ConfigService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/ConfigService.java new file mode 100644 index 0000000..047796d --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/ConfigService.java @@ -0,0 +1,22 @@ +package top.wms.admin.system.service; + +import top.continew.starter.extension.crud.service.BaseService; +import top.wms.admin.system.model.query.ConfigQuery; +import top.wms.admin.system.model.req.ConfigReq; +import top.wms.admin.system.model.resp.ConfigResp; + +/** + * 参数配置业务接口 + * + * @author zc + * @since 2025/12/30 12:44 + */ +public interface ConfigService extends BaseService { + /** + * 获取参数配置值 + * + * @param configKey 参数键名 + * @return 参数配置值 + */ + String getConfigValue(String configKey); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/DeptService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/DeptService.java new file mode 100644 index 0000000..3e488d9 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/DeptService.java @@ -0,0 +1,43 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.model.entity.DeptDO; +import top.wms.admin.system.model.query.DeptQuery; +import top.wms.admin.system.model.req.DeptReq; +import top.wms.admin.system.model.resp.DeptResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.service.BaseService; + +import java.util.List; + +/** + * 部门业务接口 + * + * @author Charles7c + * @since 2023/1/22 17:54 + */ +public interface DeptService extends BaseService, IService { + + /** + * 查询子部门列表 + * + * @param id ID + * @return 子部门列表 + */ + List listChildren(Long id); + + /** + * 通过名称查询部门 + * + * @param list 名称列表 + * @return 部门列表 + */ + List listByNames(List list); + + /** + * 通过名称查询部门数量 + * + * @param deptNames 名称列表 + * @return 部门数量 + */ + int countByNames(List deptNames); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/DictItemService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/DictItemService.java new file mode 100644 index 0000000..47c16ca --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/DictItemService.java @@ -0,0 +1,42 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.model.entity.DictItemDO; +import top.wms.admin.system.model.query.DictItemQuery; +import top.wms.admin.system.model.req.DictItemReq; +import top.wms.admin.system.model.resp.DictItemResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; +import top.continew.starter.extension.crud.service.BaseService; + +import java.util.List; + +/** + * 字典项业务接口 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +public interface DictItemService extends BaseService, IService { + + /** + * 根据字典编码查询 + * + * @param dictCode 字典编码 + * @return 字典项列表 + */ + List listByDictCode(String dictCode); + + /** + * 根据字典 ID 列表删除 + * + * @param dictIds 字典 ID 列表 + */ + void deleteByDictIds(List dictIds); + + /** + * 查询枚举字典名称列表 + * + * @return 枚举字典名称列表 + */ + List listEnumDictNames(); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/DictService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/DictService.java new file mode 100644 index 0000000..ac9d29e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/DictService.java @@ -0,0 +1,27 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.model.entity.DictDO; +import top.wms.admin.system.model.query.DictQuery; +import top.wms.admin.system.model.req.DictReq; +import top.wms.admin.system.model.resp.DictResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; +import top.continew.starter.extension.crud.service.BaseService; + +import java.util.List; + +/** + * 字典业务接口 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +public interface DictService extends BaseService, IService { + + /** + * 查询枚举字典 + * + * @return 枚举字典列表 + */ + List listEnumDict(); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/FileService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/FileService.java new file mode 100644 index 0000000..eb98cbe --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/FileService.java @@ -0,0 +1,92 @@ +package top.wms.admin.system.service; + +import org.dromara.x.file.storage.core.FileInfo; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.system.model.entity.FileDO; +import top.wms.admin.system.model.query.FileQuery; +import top.wms.admin.system.model.req.FileReq; +import top.wms.admin.system.model.resp.file.FileResp; +import top.wms.admin.system.model.resp.file.FileStatisticsResp; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.service.BaseService; + +import java.time.LocalDate; +import java.util.List; + +/** + * 文件业务接口 + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +public interface FileService extends BaseService, IService { + + /** + * 上传到默认存储 + * + * @param file 文件信息 + * @return 文件信息 + */ + default FileInfo upload(MultipartFile file, Boolean createMin) { + return upload(file, getDefaultFilePath(), null, createMin, true); + } + + /** + * 上传到默认存储 + * + * @param file 文件信息 + * @param path 文件路径 + * @return 文件信息 + */ + default FileInfo upload(MultipartFile file, String path) { + return upload(file, path, null, true, true); + } + + /** + * 上传到默认存储 + * + * @param file 文件信息 + * @param storageCode 存储编码 + * @return 文件信息 + */ + default FileInfo upload(MultipartFile file, String storageCode, Boolean createMin, Boolean store) { + return upload(file, getDefaultFilePath(), storageCode, createMin, store); + } + + /** + * 上传到指定存储 + * + * @param file 文件信息 + * @param path 文件路径 + * @param storageCode 存储编码 + * @return 文件信息 + */ + FileInfo upload(MultipartFile file, String path, String storageCode, Boolean createMin, Boolean store); + + /** + * 根据存储 ID 列表查询 + * + * @param storageIds 存储 ID 列表 + * @return 文件数量 + */ + Long countByStorageIds(List storageIds); + + /** + * 查询文件资源统计信息 + * + * @return 资源统计信息 + */ + FileStatisticsResp statistics(); + + /** + * 获取默认文件路径 + * + * @return 默认文件路径 + */ + default String getDefaultFilePath() { + LocalDate today = LocalDate.now(); + return today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today + .getDayOfMonth() + StringConstants.SLASH; + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/ImportExcelService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/ImportExcelService.java new file mode 100644 index 0000000..80575e0 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/ImportExcelService.java @@ -0,0 +1,19 @@ +package top.wms.admin.system.service; + +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public interface ImportExcelService { + + /** + * 下载导入模板 + * + * @param response 响应对象 + * @param fileName 文件名 + * @param fileNameZH 文件名中文 + * @throws IOException / + */ + void downloadImportTemplate(HttpServletResponse response, String fileName, String fileNameZH) throws IOException; + +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/LogService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/LogService.java new file mode 100644 index 0000000..bbcbeb3 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/LogService.java @@ -0,0 +1,53 @@ +package top.wms.admin.system.service; + +import jakarta.servlet.http.HttpServletResponse; +import top.wms.admin.system.model.query.LogQuery; +import top.wms.admin.system.model.resp.log.LogDetailResp; +import top.wms.admin.system.model.resp.log.LogResp; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.query.SortQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +/** + * 系统日志业务接口 + * + * @author Charles7c + * @since 2022/12/23 20:12 + */ +public interface LogService { + + /** + * 分页查询列表 + * + * @param query 查询条件 + * @param pageQuery 分页查询条件 + * @return 分页列表信息 + */ + PageResp page(LogQuery query, PageQuery pageQuery); + + /** + * 查询详情 + * + * @param id ID + * @return 详情信息 + */ + LogDetailResp get(Long id); + + /** + * 导出登录日志 + * + * @param query 查询条件 + * @param sortQuery 排序查询条件 + * @param response 响应对象 + */ + void exportLoginLog(LogQuery query, SortQuery sortQuery, HttpServletResponse response); + + /** + * 导出操作日志 + * + * @param query 查询条件 + * @param sortQuery 排序查询条件 + * @param response 响应对象 + */ + void exportOperationLog(LogQuery query, SortQuery sortQuery, HttpServletResponse response); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/MenuService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/MenuService.java new file mode 100644 index 0000000..6ce1349 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/MenuService.java @@ -0,0 +1,36 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.model.entity.MenuDO; +import top.wms.admin.system.model.query.MenuQuery; +import top.wms.admin.system.model.req.MenuReq; +import top.wms.admin.system.model.resp.MenuResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.service.BaseService; + +import java.util.List; +import java.util.Set; + +/** + * 菜单业务接口 + * + * @author Charles7c + * @since 2023/2/15 20:30 + */ +public interface MenuService extends BaseService, IService { + + /** + * 根据用户 ID 查询 + * + * @param userId 用户 ID + * @return 权限码集合 + */ + Set listPermissionByUserId(Long userId); + + /** + * 根据角色 ID 查询 + * + * @param roleId 角色 ID + * @return 菜单列表 + */ + List listByRoleId(Long roleId); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/OptionService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/OptionService.java new file mode 100644 index 0000000..358a18c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/OptionService.java @@ -0,0 +1,67 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.enums.OptionCategoryEnum; +import top.wms.admin.system.model.query.OptionQuery; +import top.wms.admin.system.model.req.OptionReq; +import top.wms.admin.system.model.req.OptionResetValueReq; +import top.wms.admin.system.model.resp.OptionResp; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * 参数业务接口 + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +public interface OptionService { + + /** + * 查询列表 + * + * @param query 查询条件 + * @return 列表信息 + */ + List list(OptionQuery query); + + /** + * 根据类别查询 + * + * @param category 类别 + * @return 参数信息 + */ + Map getByCategory(OptionCategoryEnum category); + + /** + * 修改参数 + * + * @param options 参数列表 + */ + void update(List options); + + /** + * 重置参数 + * + * @param req 重置信息 + */ + void resetValue(OptionResetValueReq req); + + /** + * 根据编码查询参数值 + * + * @param code 编码 + * @return 参数值(自动转换为 int 类型) + */ + int getValueByCode2Int(String code); + + /** + * 根据编码查询参数值 + * + * @param code 编码 + * @param mapper 转换方法 e.g.:value -> Integer.parseInt(value) + * @return 参数值 + */ + T getValueByCode(String code, Function mapper); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/PeopleEquipmentService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/PeopleEquipmentService.java new file mode 100644 index 0000000..541e325 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/PeopleEquipmentService.java @@ -0,0 +1,14 @@ +package top.wms.admin.system.service; + +import top.continew.starter.extension.crud.service.BaseService; +import top.wms.admin.system.model.query.PeopleEquipmentQuery; +import top.wms.admin.system.model.req.PeopleEquipmentReq; +import top.wms.admin.system.model.resp.PeopleEquipmentResp; + +/** + * 人员设备下发信息业务接口 + * + * @author zc + * @since 2025/03/27 14:59 + */ +public interface PeopleEquipmentService extends BaseService {} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/RoleDeptService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/RoleDeptService.java new file mode 100644 index 0000000..a6555aa --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/RoleDeptService.java @@ -0,0 +1,43 @@ +package top.wms.admin.system.service; + +import java.util.List; + +/** + * 角色和部门业务接口 + * + * @author Charles7c + * @since 2023/2/19 10:40 + */ +public interface RoleDeptService { + + /** + * 新增 + * + * @param deptIds 部门 ID 列表 + * @param roleId 角色 ID + * @return 是否新增成功(true:成功;false:无变更/失败) + */ + boolean add(List deptIds, Long roleId); + + /** + * 根据角色 ID 删除 + * + * @param roleIds 角色 ID 列表 + */ + void deleteByRoleIds(List roleIds); + + /** + * 根据部门 ID 删除 + * + * @param deptIds 部门 ID 列表 + */ + void deleteByDeptIds(List deptIds); + + /** + * 根据角色 ID 查询 + * + * @param roleId 角色 ID + * @return 部门 ID 列表 + */ + List listDeptIdByRoleId(Long roleId); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/RoleMenuService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/RoleMenuService.java new file mode 100644 index 0000000..9273b06 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/RoleMenuService.java @@ -0,0 +1,36 @@ +package top.wms.admin.system.service; + +import java.util.List; + +/** + * 角色和菜单业务接口 + * + * @author Charles7c + * @since 2023/2/19 10:40 + */ +public interface RoleMenuService { + + /** + * 新增 + * + * @param menuIds 菜单 ID 列表 + * @param roleId 角色 ID + * @return 是否新增成功(true:成功;false:无变更/失败) + */ + boolean add(List menuIds, Long roleId); + + /** + * 根据角色 ID 删除 + * + * @param roleIds 角色 ID 列表 + */ + void deleteByRoleIds(List roleIds); + + /** + * 根据角色 ID 查询 + * + * @param roleIds 角色 ID 列表 + * @return 菜单 ID 列表 + */ + List listMenuIdByRoleIds(List roleIds); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/RoleService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/RoleService.java new file mode 100644 index 0000000..c4abfb8 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/RoleService.java @@ -0,0 +1,95 @@ +package top.wms.admin.system.service; + +import top.wms.admin.common.context.RoleContext; +import top.wms.admin.system.model.entity.RoleDO; +import top.wms.admin.system.model.query.RoleQuery; +import top.wms.admin.system.model.req.RoleReq; +import top.wms.admin.system.model.req.RoleUpdatePermissionReq; +import top.wms.admin.system.model.resp.role.RoleDetailResp; +import top.wms.admin.system.model.resp.role.RoleResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.service.BaseService; + +import java.util.List; +import java.util.Set; + +/** + * 角色业务接口 + * + * @author Charles7c + * @since 2023/2/8 23:15 + */ +public interface RoleService extends BaseService, IService { + + /** + * 修改角色权限 + * + * @param id 角色 ID + * @param req 参数 + */ + void updatePermission(Long id, RoleUpdatePermissionReq req); + + /** + * 分配角色给用户 + * + * @param id 角色 ID + * @param userIds 用户 ID 列表 + */ + void assignToUsers(Long id, List userIds); + + /** + * 根据用户 ID 查询权限码 + * + * @param userId 用户 ID + * @return 权限码集合 + */ + Set listPermissionByUserId(Long userId); + + /** + * 根据 ID 列表查询 + * + * @param ids ID 列表 + * @return 名称列表 + */ + List listNameByIds(List ids); + + /** + * 根据用户 ID 查询角色编码 + * + * @param userId 用户 ID + * @return 角色编码集合 + */ + Set listCodeByUserId(Long userId); + + /** + * 根据用户 ID 查询角色 + * + * @param userId 用户 ID + * @return 角色集合 + */ + Set listByUserId(Long userId); + + /** + * 根据角色编码查询 + * + * @param code 角色编码 + * @return 角色信息 + */ + RoleDO getByCode(String code); + + /** + * 根据角色名称查询 + * + * @param list 名称列表 + * @return 角色列表 + */ + List listByNames(List list); + + /** + * 根据角色名称查询数量 + * + * @param roleNames 名称列表 + * @return 角色数量 + */ + int countByNames(List roleNames); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/StorageService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/StorageService.java new file mode 100644 index 0000000..fe267da --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/StorageService.java @@ -0,0 +1,62 @@ +package top.wms.admin.system.service; + +import top.wms.admin.common.model.req.CommonStatusUpdateReq; +import top.wms.admin.system.model.entity.StorageDO; +import top.wms.admin.system.model.query.StorageQuery; +import top.wms.admin.system.model.req.StorageReq; +import top.wms.admin.system.model.resp.StorageResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.service.BaseService; + +/** + * 存储业务接口 + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +public interface StorageService extends BaseService, IService { + + /** + * 修改状态 + * + * @param req 状态信息 + * @param id ID + */ + void updateStatus(CommonStatusUpdateReq req, Long id); + + /** + * 设置默认存储 + * + * @param id ID + */ + void setDefault(Long id); + + /** + * 查询默认存储 + * + * @return 存储信息 + */ + StorageDO getDefaultStorage(); + + /** + * 根据编码查询 + * + * @param code 编码 + * @return 存储信息 + */ + StorageDO getByCode(String code); + + /** + * 加载存储 + * + * @param req 存储信息 + */ + void load(StorageReq req); + + /** + * 卸载存储 + * + * @param req 存储信息 + */ + void unload(StorageReq req); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/UserPasswordHistoryService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/UserPasswordHistoryService.java new file mode 100644 index 0000000..6ecbc91 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/UserPasswordHistoryService.java @@ -0,0 +1,38 @@ +package top.wms.admin.system.service; + +import java.util.List; + +/** + * 用户历史密码业务接口 + * + * @author Charles7c + * @since 2024/5/16 21:58 + */ +public interface UserPasswordHistoryService { + + /** + * 新增 + * + * @param userId 用户 ID + * @param password 密码 + * @param count 保留 N 个历史 + */ + void add(Long userId, String password, int count); + + /** + * 根据用户 ID 删除 + * + * @param userIds 用户 ID 列表 + */ + void deleteByUserIds(List userIds); + + /** + * 密码是否为重复使用 + * + * @param userId 用户 ID + * @param password 密码 + * @param count 最近 N 次 + * @return 是否为重复使用 + */ + boolean isPasswordReused(Long userId, String password, int count); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/UserRoleService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/UserRoleService.java new file mode 100644 index 0000000..0c765c8 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/UserRoleService.java @@ -0,0 +1,90 @@ +package top.wms.admin.system.service; + +import top.wms.admin.system.model.entity.UserRoleDO; +import top.wms.admin.system.model.query.RoleUserQuery; +import top.wms.admin.system.model.resp.role.RoleUserResp; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; + +/** + * 用户和角色业务接口 + * + * @author Charles7c + * @since 2023/2/20 21:30 + */ +public interface UserRoleService { + + /** + * 分页查询角色关联用户列表 + * + * @param query 查询条件 + * @param pageQuery 分页查询条件 + * @return 分页信息 + */ + PageResp pageUser(RoleUserQuery query, PageQuery pageQuery); + + /** + * 批量分配角色给指定用户 + * + * @param roleIds 角色 ID 列表 + * @param userId 用户 ID + * @return 是否成功(true:成功;false:无变更/失败) + */ + boolean assignRolesToUser(List roleIds, Long userId); + + /** + * 批量分配角色给用户 + * + * @param roleId 角色 ID + * @param userIds 用户 ID 列表 + * @return 是否成功(true:成功;false:无变更/失败) + */ + boolean assignRoleToUsers(Long roleId, List userIds); + + /** + * 根据 ID 删除 + * + * @param ids ID 列表 + */ + void deleteByIds(List ids); + + /** + * 根据用户 ID 删除 + * + * @param userIds 用户 ID 列表 + */ + void deleteByUserIds(List userIds); + + /** + * 批量插入 + * + * @param list 数据集 + */ + void saveBatch(List list); + + /** + * 根据用户 ID 查询 + * + * @param userId 用户 ID + * @return 角色 ID 列表 + */ + List listRoleIdByUserId(Long userId); + + /** + * 根据角色 ID 查询 + * + * @param roleId 角色 ID + * @return 用户 ID 列表 + */ + List listUserIdByRoleId(Long roleId); + + /** + * 根据角色 ID 判断是否已被用户关联 + * + * @param roleIds 角色 ID 列表 + * @return 是否已关联(true:已关联;false:未关联) + */ + boolean isRoleIdExists(List roleIds); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/UserService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/UserService.java new file mode 100644 index 0000000..5e43cc7 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/UserService.java @@ -0,0 +1,154 @@ +package top.wms.admin.system.service; + +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.query.UserQuery; +import top.wms.admin.system.model.req.user.*; +import top.wms.admin.system.model.resp.user.UserDetailResp; +import top.wms.admin.system.model.resp.user.UserImportParseResp; +import top.wms.admin.system.model.resp.user.UserImportResp; +import top.wms.admin.system.model.resp.user.UserResp; +import top.continew.starter.data.mp.service.IService; +import top.continew.starter.extension.crud.service.BaseService; + +import java.io.IOException; +import java.util.List; + +/** + * 用户业务接口 + * + * @author Charles7c + * @since 2022/12/21 21:48 + */ +public interface UserService extends BaseService, IService { + + /** + * 下载导入模板 + * + * @param response 响应对象 + * @throws IOException / + */ + void downloadImportTemplate(HttpServletResponse response) throws IOException; + + /** + * 解析导入数据 + * + * @param file 导入文件 + * @return 解析结果 + */ + UserImportParseResp parseImport(MultipartFile file); + + /** + * 导入数据 + * + * @param req 导入信息 + * @return 导入结果 + */ + UserImportResp importUser(UserImportReq req); + + /** + * 重置密码 + * + * @param req 重置信息 + * @param id ID + */ + void resetPassword(UserPasswordResetReq req, Long id); + + /** + * 修改角色 + * + * @param updateReq 修改信息 + * @param id ID + */ + void updateRole(UserRoleUpdateReq updateReq, Long id); + + /** + * 上传头像 + * + * @param avatar 头像文件 + * @param id ID + * @return 新头像路径 + * @throws IOException / + */ + String updateAvatar(MultipartFile avatar, Long id) throws IOException; + + /** + * 修改基础信息 + * + * @param req 修改信息 + * @param id ID + */ + void updateBasicInfo(UserBasicInfoUpdateReq req, Long id); + + /** + * 修改密码 + * + * @param oldPassword 当前密码 + * @param newPassword 新密码 + * @param id ID + */ + void updatePassword(String oldPassword, String newPassword, Long id); + + /** + * 修改手机号 + * + * @param newPhone 新手机号 + * @param oldPassword 当前密码 + * @param id ID + */ + void updatePhone(String newPhone, String oldPassword, Long id); + + /** + * 修改邮箱 + * + * @param newEmail 新邮箱 + * @param oldPassword 当前密码 + * @param id ID + */ + void updateEmail(String newEmail, String oldPassword, Long id); + + /** + * 根据用户名查询 + * + * @param username 用户名 + * @return 用户信息 + */ + UserDO getByUsername(String username); + + /** + * 根据手机号查询 + * + * @param phone 手机号 + * @return 用户信息 + */ + UserDO getByPhone(String phone); + + /** + * 根据邮箱查询 + * + * @param email 邮箱 + * @return 用户信息 + */ + UserDO getByEmail(String email); + + /** + * 根据卡片查询 + * + * @param card 卡片 + * @return 用户信息 + */ + UserDO getByCard(String card); + + /** + * 根据部门 ID 列表查询 + * + * @param deptIds 部门 ID 列表 + * @return 用户数量 + */ + Long countByDeptIds(List deptIds); + + List listNameByIds(List ids); + + String userNameByIds(Long id); +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/UserSocialService.java b/mes-module-system/src/main/java/top/mes/admin/system/service/UserSocialService.java new file mode 100644 index 0000000..5e8682c --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/UserSocialService.java @@ -0,0 +1,55 @@ +package top.wms.admin.system.service; + +import me.zhyd.oauth.model.AuthUser; +import top.wms.admin.system.model.entity.UserSocialDO; + +import java.util.List; + +/** + * 用户社会化关联业务接口 + * + * @author Charles7c + * @since 2023/10/11 22:10 + */ +public interface UserSocialService { + + /** + * 根据来源和开放 ID 查询 + * + * @param source 来源 + * @param openId 开放 ID + * @return 用户社会化关联信息 + */ + UserSocialDO getBySourceAndOpenId(String source, String openId); + + /** + * 保存 + * + * @param userSocial 用户社会化关联信息 + */ + void saveOrUpdate(UserSocialDO userSocial); + + /** + * 根据用户 ID 查询 + * + * @param userId 用户 ID + * @return 用户社会化关联信息 + */ + List listByUserId(Long userId); + + /** + * 绑定 + * + * @param authUser 三方账号信息 + * @param userId 用户 ID + */ + void bind(AuthUser authUser, Long userId); + + /** + * 根据来源和用户 ID 删除 + * + * @param source 来源 + * @param userId 用户 ID + */ + void deleteBySourceAndUserId(String source, Long userId); +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ClientServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ClientServiceImpl.java new file mode 100644 index 0000000..ed25c9b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ClientServiceImpl.java @@ -0,0 +1,59 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.crypto.digest.DigestUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.auth.model.query.OnlineUserQuery; +import top.wms.admin.auth.service.OnlineUserService; +import top.wms.admin.system.mapper.ClientMapper; +import top.wms.admin.system.model.entity.ClientDO; +import top.wms.admin.system.model.query.ClientQuery; +import top.wms.admin.system.model.req.ClientReq; +import top.wms.admin.system.model.resp.ClientResp; +import top.wms.admin.system.service.ClientService; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import java.util.List; + +/** + * 终端业务实现 + * + * @author KAI + * @author Charles7c + * @since 2024/12/03 16:04 + */ +@Service +@RequiredArgsConstructor +public class ClientServiceImpl extends BaseServiceImpl implements ClientService { + + private final OnlineUserService onlineUserService; + + @Override + public void beforeAdd(ClientReq req) { + String clientId = DigestUtil.md5Hex(req.getClientKey() + StringConstants.COLON + req.getClientSecret()); + req.setClientId(clientId); + } + + @Override + public void beforeDelete(List ids) { + // 如果还存在在线用户,则不能删除 + OnlineUserQuery query = new OnlineUserQuery(); + for (Long id : ids) { + ClientDO client = this.getById(id); + query.setClientId(client.getClientId()); + CheckUtils.throwIfNotEmpty(onlineUserService.list(query), "终端 [{}] 还存在在线用户,不能删除", client.getClientKey()); + } + } + + @Override + public ClientResp getByClientId(String clientId) { + return baseMapper.lambdaQuery() + .eq(ClientDO::getClientId, clientId) + .oneOpt() + .map(client -> BeanUtil.copyProperties(client, ClientResp.class)) + .orElse(null); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ConfigServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ConfigServiceImpl.java new file mode 100644 index 0000000..bf285da --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ConfigServiceImpl.java @@ -0,0 +1,63 @@ +package top.wms.admin.system.service.impl; + +import com.alicp.jetcache.anno.Cached; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.mapper.ConfigMapper; +import top.wms.admin.system.model.entity.ConfigDO; +import top.wms.admin.system.model.query.ConfigQuery; +import top.wms.admin.system.model.req.ConfigReq; +import top.wms.admin.system.model.resp.ConfigResp; +import top.wms.admin.system.service.ConfigService; + +import java.time.Duration; +import java.util.List; + +/** + * 参数配置业务实现 + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Service +@RequiredArgsConstructor +public class ConfigServiceImpl extends BaseServiceImpl implements ConfigService { + + @Override + @Transactional(rollbackFor = Exception.class) + public void afterAdd(ConfigReq configReq, ConfigDO configDO) { + RedisUtils.set(CacheConstants.SYS_CONFIG_KEY + configDO.getConfigKey(), configDO.getConfigValue(), Duration + .ofDays(30)); + } + + @Override + @Cached(name = CacheConstants.SYS_CONFIG_KEY, key = "#configKey") + public String getConfigValue(String configKey) { + ConfigDO configDO = baseMapper.selectOne(new LambdaQueryWrapper() + .eq(ConfigDO::getConfigKey, configKey)); + return configDO.getConfigValue(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void afterUpdate(ConfigReq configReq, ConfigDO configDO) { + // 先删除缓存 + RedisUtils.delete(CacheConstants.SYS_CONFIG_KEY + configReq.getConfigKey()); + // 再设置缓存 + RedisUtils.set(CacheConstants.SYS_CONFIG_KEY + configDO.getConfigKey(), configDO.getConfigValue(), Duration + .ofDays(30)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void beforeDelete(List Ids) { + List configDOS = baseMapper.selectByIds(Ids); + configDOS.forEach(config -> RedisUtils.delete(CacheConstants.SYS_CONFIG_KEY + config.getConfigKey())); + } + +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DeptServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DeptServiceImpl.java new file mode 100644 index 0000000..c4bd6ca --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DeptServiceImpl.java @@ -0,0 +1,190 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.mapper.DeptMapper; +import top.wms.admin.system.model.entity.DeptDO; +import top.wms.admin.system.model.query.DeptQuery; +import top.wms.admin.system.model.req.DeptReq; +import top.wms.admin.system.model.resp.DeptResp; +import top.wms.admin.system.service.DeptService; +import top.wms.admin.system.service.RoleDeptService; +import top.wms.admin.system.service.UserService; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.data.core.enums.DatabaseType; +import top.continew.starter.data.core.util.MetaUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 部门业务实现 + * + * @author Charles7c + * @since 2023/1/22 17:55 + */ +@Service +@RequiredArgsConstructor +public class DeptServiceImpl extends BaseServiceImpl implements DeptService { + + private final RoleDeptService roleDeptService; + @Resource + private UserService userService; + @Resource + private DataSource dataSource; + + @Override + public void beforeAdd(DeptReq req) { + String name = req.getName(); + boolean isExists = this.isNameExists(name, req.getParentId(), null); + CheckUtils.throwIf(isExists, "新增失败,[{}] 已存在", name); + req.setAncestors(this.getAncestors(req.getParentId())); + } + + @Override + public void beforeUpdate(DeptReq req, Long id) { + String name = req.getName(); + boolean isExists = this.isNameExists(name, req.getParentId(), id); + CheckUtils.throwIf(isExists, "修改失败,[{}] 已存在", name); + DeptDO oldDept = super.getById(id); + String oldName = oldDept.getName(); + DisEnableStatusEnum newStatus = req.getStatus(); + Long oldParentId = oldDept.getParentId(); + if (Boolean.TRUE.equals(oldDept.getIsSystem())) { + CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, newStatus, "[{}] 是系统内置部门,不允许禁用", oldName); + CheckUtils.throwIfNotEqual(req.getParentId(), oldParentId, "[{}] 是系统内置部门,不允许变更上级部门", oldName); + } + // 启用/禁用部门 + if (ObjectUtil.notEqual(newStatus, oldDept.getStatus())) { + List children = this.listChildren(id); + long enabledChildrenCount = children.stream() + .filter(d -> DisEnableStatusEnum.ENABLE.equals(d.getStatus())) + .count(); + CheckUtils.throwIf(DisEnableStatusEnum.DISABLE + .equals(newStatus) && enabledChildrenCount > 0, "禁用 [{}] 前,请先禁用其所有下级部门", oldName); + DeptDO oldParentDept = this.getByParentId(oldParentId); + CheckUtils.throwIf(DisEnableStatusEnum.ENABLE.equals(newStatus) && DisEnableStatusEnum.DISABLE + .equals(oldParentDept.getStatus()), "启用 [{}] 前,请先启用其所有上级部门", oldName); + } + // 变更上级部门 + if (ObjectUtil.notEqual(req.getParentId(), oldParentId)) { + // 更新祖级列表 + String newAncestors = this.getAncestors(req.getParentId()); + req.setAncestors(newAncestors); + // 更新子级的祖级列表 + this.updateChildrenAncestors(newAncestors, oldDept.getAncestors(), id); + } + } + + @Override + public void beforeDelete(List ids) { + + } + + @Override + public List listChildren(Long id) { + DatabaseType databaseType = MetaUtils.getDatabaseTypeOrDefault(dataSource, DatabaseType.MYSQL); + return baseMapper.lambdaQuery().apply(databaseType.findInSet(id, "ancestors")).list(); + } + + @Override + public List listByNames(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + return this.list(Wrappers.lambdaQuery().in(DeptDO::getName, list)); + } + + @Override + public int countByNames(List deptNames) { + if (CollUtil.isEmpty(deptNames)) { + return 0; + } + return (int)this.count(Wrappers.lambdaQuery().in(DeptDO::getName, deptNames)); + } + + /** + * 名称是否存在 + * + * @param name 名称 + * @param parentId 上级 ID + * @param id ID + * @return 是否存在 + */ + private boolean isNameExists(String name, Long parentId, Long id) { + return baseMapper.lambdaQuery() + .eq(DeptDO::getName, name) + .eq(DeptDO::getParentId, parentId) + .ne(null != id, DeptDO::getId, id) + .exists(); + } + + /** + * 获取祖级列表 + * + * @param parentId 上级部门 + * @return 祖级列表 + */ + private String getAncestors(Long parentId) { + DeptDO parentDept = this.getByParentId(parentId); + return "%s,%s".formatted(parentDept.getAncestors(), parentId); + } + + /** + * 根据上级部门 ID 查询 + * + * @param parentId 上级部门 ID + * @return 上级部门信息 + */ + private DeptDO getByParentId(Long parentId) { + DeptDO parentDept = baseMapper.selectById(parentId); + CheckUtils.throwIfNull(parentDept, "上级部门不存在"); + return parentDept; + } + + /** + * 查询子部门数量 + * + * @param ids ID 列表 + * @return 子部门数量 + */ + private Long countChildren(List ids) { + if (CollUtil.isEmpty(ids)) { + return 0L; + } + DatabaseType databaseType = MetaUtils.getDatabaseTypeOrDefault(dataSource, DatabaseType.MYSQL); + return ids.stream() + .mapToLong(id -> baseMapper.lambdaQuery().apply(databaseType.findInSet(id, "ancestors")).count()) + .sum(); + } + + /** + * 更新子部门祖级列表 + * + * @param newAncestors 新祖级列表 + * @param oldAncestors 原祖级列表 + * @param id ID + */ + private void updateChildrenAncestors(String newAncestors, String oldAncestors, Long id) { + List children = this.listChildren(id); + if (CollUtil.isEmpty(children)) { + return; + } + List list = new ArrayList<>(children.size()); + for (DeptDO child : children) { + DeptDO dept = new DeptDO(); + dept.setId(child.getId()); + dept.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + list.add(dept); + } + baseMapper.updateById(list); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DictItemServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DictItemServiceImpl.java new file mode 100644 index 0000000..2b4c1b6 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DictItemServiceImpl.java @@ -0,0 +1,118 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.mapper.DictItemMapper; +import top.wms.admin.system.model.entity.DictItemDO; +import top.wms.admin.system.model.query.DictItemQuery; +import top.wms.admin.system.model.req.DictItemReq; +import top.wms.admin.system.model.resp.DictItemResp; +import top.wms.admin.system.service.DictItemService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.autoconfigure.project.ProjectProperties; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.enums.BaseEnum; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 字典项业务实现 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DictItemServiceImpl extends BaseServiceImpl implements DictItemService { + + private final ProjectProperties projectProperties; + private static final Map> ENUM_DICT_CACHE = new ConcurrentHashMap<>(); + + @Override + public void beforeAdd(DictItemReq req) { + String value = req.getValue(); + CheckUtils.throwIf(this.isValueExists(value, null, req.getDictId()), "新增失败,字典值 [{}] 已存在", value); + RedisUtils.deleteByPattern(CacheConstants.DICT_KEY_PREFIX + StringConstants.ASTERISK); + } + + @Override + public void beforeUpdate(DictItemReq req, Long id) { + String value = req.getValue(); + CheckUtils.throwIf(this.isValueExists(value, id, req.getDictId()), "修改失败,字典值 [{}] 已存在", value); + RedisUtils.deleteByPattern(CacheConstants.DICT_KEY_PREFIX + StringConstants.ASTERISK); + } + + @Override + public List listByDictCode(String dictCode) { + return Optional.ofNullable(ENUM_DICT_CACHE.get(dictCode.toLowerCase())) + .orElseGet(() -> baseMapper.listByDictCode(dictCode)); + } + + @Override + public void deleteByDictIds(List dictIds) { + if (CollUtil.isEmpty(dictIds)) { + return; + } + baseMapper.lambdaUpdate().in(DictItemDO::getDictId, dictIds).remove(); + RedisUtils.deleteByPattern(CacheConstants.DICT_KEY_PREFIX + StringConstants.ASTERISK); + } + + @Override + public List listEnumDictNames() { + return ENUM_DICT_CACHE.keySet().stream().toList(); + } + + /** + * 字典值是否存在 + * + * @param value 字典值 + * @param id ID + * @param dictId 字典 ID + * @return 是否存在 + */ + private boolean isValueExists(String value, Long id, Long dictId) { + return baseMapper.lambdaQuery() + .eq(DictItemDO::getValue, value) + .eq(DictItemDO::getDictId, dictId) + .ne(null != id, DictItemDO::getId, id) + .exists(); + } + + /** + * 将枚举转换为枚举字典 + * + * @param enumClass 枚举类型 + * @return 枚举字典 + */ + private List toEnumDict(Class enumClass) { + Object[] enumConstants = enumClass.getEnumConstants(); + return Arrays.stream(enumConstants).map(e -> { + BaseEnum baseEnum = (BaseEnum)e; + return new LabelValueResp(baseEnum.getDescription(), baseEnum.getValue(), baseEnum.getColor()); + }).toList(); + } + + /** + * 缓存枚举字典 + */ + @PostConstruct + public void init() { + Set> classSet = ClassUtil.scanPackageBySuper(projectProperties.getBasePackage(), BaseEnum.class); + ENUM_DICT_CACHE.putAll(classSet.stream() + .collect(Collectors.toMap(cls -> StrUtil.toUnderlineCase(cls.getSimpleName()) + .toLowerCase(), this::toEnumDict))); + log.debug("枚举字典已缓存到内存:{}", ENUM_DICT_CACHE.keySet()); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DictServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DictServiceImpl.java new file mode 100644 index 0000000..8a7b427 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/DictServiceImpl.java @@ -0,0 +1,86 @@ +package top.wms.admin.system.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.system.mapper.DictMapper; +import top.wms.admin.system.model.entity.DictDO; +import top.wms.admin.system.model.query.DictQuery; +import top.wms.admin.system.model.req.DictReq; +import top.wms.admin.system.model.resp.DictResp; +import top.wms.admin.system.service.DictItemService; +import top.wms.admin.system.service.DictService; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import java.util.List; +import java.util.Optional; + +/** + * 字典业务实现 + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Service +@RequiredArgsConstructor +public class DictServiceImpl extends BaseServiceImpl implements DictService { + + private final DictItemService dictItemService; + + @Override + public void beforeAdd(DictReq req) { + String name = req.getName(); + CheckUtils.throwIf(this.isNameExists(name, null), "新增失败,[{}] 已存在", name); + String code = req.getCode(); + CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); + } + + @Override + public void beforeUpdate(DictReq req, Long id) { + String name = req.getName(); + CheckUtils.throwIf(this.isNameExists(name, id), "修改失败,[{}] 已存在", name); + DictDO oldDict = super.getById(id); + CheckUtils.throwIfNotEqual(req.getCode(), oldDict.getCode(), "不允许修改字典编码"); + } + + @Override + public void beforeDelete(List ids) { + List list = baseMapper.lambdaQuery() + .select(DictDO::getName, DictDO::getIsSystem) + .in(DictDO::getId, ids) + .list(); + Optional isSystemData = list.stream().filter(DictDO::getIsSystem).findFirst(); + CheckUtils.throwIf(isSystemData::isPresent, "所选字典 [{}] 是系统内置字典,不允许删除", isSystemData.orElseGet(DictDO::new) + .getName()); + dictItemService.deleteByDictIds(ids); + } + + @Override + public List listEnumDict() { + List enumDictNameList = dictItemService.listEnumDictNames(); + return enumDictNameList.stream().map(name -> new LabelValueResp(name, name)).toList(); + } + + /** + * 名称是否存在 + * + * @param name 名称 + * @param id ID + * @return 是否存在 + */ + private boolean isNameExists(String name, Long id) { + return baseMapper.lambdaQuery().eq(DictDO::getName, name).ne(null != id, DictDO::getId, id).exists(); + } + + /** + * 编码是否存在 + * + * @param code 编码 + * @param id ID + * @return 是否存在 + */ + private boolean isCodeExists(String code, Long id) { + return baseMapper.lambdaQuery().eq(DictDO::getCode, code).ne(null != id, DictDO::getId, id).exists(); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/FileServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..5f79e48 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/FileServiceImpl.java @@ -0,0 +1,154 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.dromara.x.file.storage.core.ProgressListener; +import org.dromara.x.file.storage.core.upload.UploadPretreatment; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.system.enums.FileTypeEnum; +import top.wms.admin.system.mapper.FileMapper; +import top.wms.admin.system.model.entity.FileDO; +import top.wms.admin.system.model.entity.StorageDO; +import top.wms.admin.system.model.query.FileQuery; +import top.wms.admin.system.model.req.FileReq; +import top.wms.admin.system.model.resp.file.FileResp; +import top.wms.admin.system.model.resp.file.FileStatisticsResp; +import top.wms.admin.system.service.FileService; +import top.wms.admin.system.service.StorageService; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.StrUtils; +import top.continew.starter.core.util.URLUtils; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 文件业务实现 + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileServiceImpl extends BaseServiceImpl implements FileService { + + private final FileStorageService fileStorageService; + @Resource + private StorageService storageService; + + @Override + protected void beforeDelete(List ids) { + List fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list(); + Map> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); + for (Map.Entry> entry : fileListGroup.entrySet()) { + StorageDO storage = storageService.getById(entry.getKey()); + for (FileDO file : entry.getValue()) { + FileInfo fileInfo = file.toFileInfo(storage); + fileStorageService.delete(fileInfo); + } + } + } + + @Override + public FileInfo upload(MultipartFile file, String path, String storageCode, Boolean createMin, Boolean store) { + StorageDO storage; + if (StrUtil.isBlank(storageCode)) { + storage = storageService.getDefaultStorage(); + CheckUtils.throwIfNull(storage, "请先指定默认存储"); + } else { + storage = storageService.getByCode(storageCode); + CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", storageCode); + } + UploadPretreatment uploadPretreatment = fileStorageService.of(file) + .setPlatform(storage.getCode()) + .setHashCalculatorMd5(true) + .putAttr(ClassUtil.getClassName(StorageDO.class, false), storage) + .setPath(path) + .setObjectType(String.valueOf(store)); + // 图片文件生成缩略图 + if (createMin && FileTypeEnum.IMAGE.getExtensions() + .contains(FileNameUtil.extName(file.getOriginalFilename()))) { + uploadPretreatment.thumbnail(img -> img.size(100, 100)); + } + uploadPretreatment.setProgressMonitor(new ProgressListener() { + @Override + public void start() { + log.info("开始上传"); + } + + @Override + public void progress(long progressSize, Long allSize) { + log.info("已上传 [{}],总大小 [{}]", progressSize, allSize); + } + + @Override + public void finish() { + log.info("上传结束"); + } + }); + // 处理本地存储文件 URL + FileInfo fileInfo = null; + try { + fileInfo = uploadPretreatment.upload(); + String domain = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH); + fileInfo.setUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getFilename())); + if (StrUtil.isNotBlank(fileInfo.getThFilename())) { + fileInfo.setThUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getThFilename())); + } else { + fileInfo.setThUrl(fileInfo.getUrl()); + } + } catch (Exception e) { + + } + return fileInfo; + } + + @Override + public Long countByStorageIds(List storageIds) { + if (CollUtil.isEmpty(storageIds)) { + return 0L; + } + return baseMapper.lambdaQuery().in(FileDO::getStorageId, storageIds).count(); + } + + @Override + public FileStatisticsResp statistics() { + FileStatisticsResp resp = new FileStatisticsResp(); + List statisticsList = baseMapper.statistics(); + if (CollUtil.isEmpty(statisticsList)) { + return resp; + } + resp.setData(statisticsList); + resp.setSize(statisticsList.stream().mapToLong(FileStatisticsResp::getSize).sum()); + resp.setNumber(statisticsList.stream().mapToLong(FileStatisticsResp::getNumber).sum()); + return resp; + } + + @Override + protected void fill(Object obj) { + super.fill(obj); + if (obj instanceof FileResp fileResp && !URLUtils.isHttpUrl(fileResp.getUrl())) { + StorageDO storage = storageService.getById(fileResp.getStorageId()); + String prefix = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH); + String url = URLUtil.normalize(prefix + fileResp.getUrl()); + fileResp.setUrl(url); + String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailUrl(), url, thUrl -> URLUtil + .normalize(prefix + thUrl)); + fileResp.setThumbnailUrl(thumbnailUrl); + fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode())); + } + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ImportExcelServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ImportExcelServiceImpl.java new file mode 100644 index 0000000..8278fb6 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/ImportExcelServiceImpl.java @@ -0,0 +1,39 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.ContentType; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import net.dreamlu.mica.core.result.R; +import top.continew.starter.web.util.FileUploadUtils; +import top.wms.admin.system.service.ImportExcelService; + +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 导入Excel服务实现类 + * + * @author zc + * @since 2025/03/21 18:10 + */ +@Service +@Slf4j +public class ImportExcelServiceImpl implements ImportExcelService { + + @Override + public void downloadImportTemplate(HttpServletResponse response, + String fileName, + String fileNameZH) throws IOException { + try { + FileUploadUtils.download(response, ResourceUtil.getStream("templates/import/" + fileName), fileNameZH); + } catch (Exception e) { + log.error("下载导入模板失败:{}", e.getMessage(), e); + response.setCharacterEncoding(CharsetUtil.UTF_8); + response.setContentType(ContentType.JSON.toString()); + response.getWriter().write(JSONUtil.toJsonStr(R.fail("下载导入模板失败"))); + } + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/LogServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/LogServiceImpl.java new file mode 100644 index 0000000..bea9cbe --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/LogServiceImpl.java @@ -0,0 +1,114 @@ +package top.wms.admin.system.service.impl; + +import cn.crane4j.annotation.AutoOperate; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.mapper.LogMapper; +import top.wms.admin.system.model.entity.LogDO; +import top.wms.admin.system.model.query.LogQuery; +import top.wms.admin.system.model.resp.log.LogDetailResp; +import top.wms.admin.system.model.resp.log.LogResp; +import top.wms.admin.system.model.resp.log.LoginLogExportResp; +import top.wms.admin.system.model.resp.log.OperationLogExportResp; +import top.wms.admin.system.service.LogService; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.data.mp.util.QueryWrapperHelper; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.query.SortQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; +import top.continew.starter.file.excel.util.ExcelUtils; + +import java.util.Date; +import java.util.List; + +/** + * 系统日志业务实现 + * + * @author Charles7c + * @since 2022/12/23 20:12 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LogServiceImpl implements LogService { + + private final LogMapper baseMapper; + + @Override + public PageResp page(LogQuery query, PageQuery pageQuery) { + QueryWrapper queryWrapper = this.buildQueryWrapper(query); + QueryWrapperHelper.sort(queryWrapper, pageQuery.getSort()); + IPage page = baseMapper.selectLogPage(new Page<>(pageQuery.getPage(), pageQuery + .getSize()), queryWrapper); + return PageResp.build(page); + } + + @Override + @AutoOperate(type = LogDetailResp.class) + public LogDetailResp get(Long id) { + LogDO logDO = baseMapper.selectById(id); + CheckUtils.throwIfNotExists(logDO, "LogDO", "ID", id); + return BeanUtil.copyProperties(logDO, LogDetailResp.class); + } + + @Override + public void exportLoginLog(LogQuery query, SortQuery sortQuery, HttpServletResponse response) { + List list = BeanUtil.copyToList(this.list(query, sortQuery), LoginLogExportResp.class); + ExcelUtils.export(list, "导出登录日志数据", LoginLogExportResp.class, response); + } + + @Override + public void exportOperationLog(LogQuery query, SortQuery sortQuery, HttpServletResponse response) { + List list = BeanUtil.copyToList(this + .list(query, sortQuery), OperationLogExportResp.class); + ExcelUtils.export(list, "导出操作日志数据", OperationLogExportResp.class, response); + } + + /** + * 查询列表 + * + * @param query 查询条件 + * @param sortQuery 排序查询条件 + * @return 列表信息 + */ + private List list(LogQuery query, SortQuery sortQuery) { + QueryWrapper queryWrapper = this.buildQueryWrapper(query); + QueryWrapperHelper.sort(queryWrapper, sortQuery.getSort()); + return baseMapper.selectLogList(queryWrapper); + } + + /** + * 构建 QueryWrapper + * + * @param query 查询条件 + * @return QueryWrapper + */ + private QueryWrapper buildQueryWrapper(LogQuery query) { + String description = query.getDescription(); + String module = query.getModule(); + String ip = query.getIp(); + String createUserString = query.getCreateUserString(); + DisEnableStatusEnum status = query.getStatus(); + List createTimeList = query.getCreateTime(); + return new QueryWrapper().and(StrUtil.isNotBlank(description), q -> q.like("t1.description", description) + .or() + .like("t1.module", description)) + .eq(StrUtil.isNotBlank(module), "t1.module", module) + .and(StrUtil.isNotBlank(ip), q -> q.like("t1.ip", ip).or().like("t1.address", ip)) + .and(StrUtil.isNotBlank(createUserString), q -> q.like("t2.username", createUserString) + .or() + .like("t2.nickname", createUserString)) + .eq(null != status, "t1.status", status) + .between(CollUtil.isNotEmpty(createTimeList), "t1.create_time", CollUtil.getFirst(createTimeList), CollUtil + .getLast(createTimeList)); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/MenuServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/MenuServiceImpl.java new file mode 100644 index 0000000..d3b4faf --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/MenuServiceImpl.java @@ -0,0 +1,124 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.alicp.jetcache.anno.Cached; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.system.enums.MenuTypeEnum; +import top.wms.admin.system.mapper.MenuMapper; +import top.wms.admin.system.model.entity.MenuDO; +import top.wms.admin.system.model.query.MenuQuery; +import top.wms.admin.system.model.req.MenuReq; +import top.wms.admin.system.model.resp.MenuResp; +import top.wms.admin.system.service.MenuService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import java.util.List; +import java.util.Set; + +/** + * 菜单业务实现 + * + * @author Charles7c + * @since 2023/2/15 20:30 + */ +@Service +@RequiredArgsConstructor +public class MenuServiceImpl extends BaseServiceImpl implements MenuService { + + @Override + public Long add(MenuReq req) { + String title = req.getTitle(); + CheckUtils.throwIf(this.isTitleExists(title, req.getParentId(), null), "新增失败,标题 [{}] 已存在", title); + // 目录和菜单的组件名称不能重复 + if (!MenuTypeEnum.BUTTON.equals(req.getType())) { + String name = req.getName(); + CheckUtils.throwIf(this.isNameExists(name, null), "新增失败,组件名称 [{}] 已存在", name); + } + // 目录类型菜单,默认为 Layout + if (MenuTypeEnum.DIR.equals(req.getType())) { + req.setComponent(StrUtil.blankToDefault(req.getComponent(), "Layout")); + } + RedisUtils.deleteByPattern(CacheConstants.ROLE_MENU_KEY_PREFIX + StringConstants.ASTERISK); + return super.add(req); + } + + @Override + public void update(MenuReq req, Long id) { + String title = req.getTitle(); + CheckUtils.throwIf(this.isTitleExists(title, req.getParentId(), id), "修改失败,标题 [{}] 已存在", title); + // 目录和菜单的组件名称不能重复 + if (!MenuTypeEnum.BUTTON.equals(req.getType())) { + String name = req.getName(); + CheckUtils.throwIf(this.isNameExists(name, id), "修改失败,组件名称 [{}] 已存在", name); + } + MenuDO oldMenu = super.getById(id); + CheckUtils.throwIfNotEqual(req.getType(), oldMenu.getType(), "不允许修改菜单类型"); + super.update(req, id); + RedisUtils.deleteByPattern(CacheConstants.ROLE_MENU_KEY_PREFIX + StringConstants.ASTERISK); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(List ids) { + baseMapper.lambdaUpdate().in(MenuDO::getParentId, ids).remove(); + super.delete(ids); + RedisUtils.deleteByPattern(CacheConstants.ROLE_MENU_KEY_PREFIX + StringConstants.ASTERISK); + } + + @Override + public Set listPermissionByUserId(Long userId) { + return baseMapper.selectPermissionByUserId(userId); + } + + @Override + @Cached(key = "#roleId", name = CacheConstants.ROLE_MENU_KEY_PREFIX) + public List listByRoleId(Long roleId) { + if (SysConstants.SUPER_ROLE_ID.equals(roleId)) { + return super.list(new MenuQuery(DisEnableStatusEnum.ENABLE), null); + } + List menuList = baseMapper.selectListByRoleId(roleId); + List list = BeanUtil.copyToList(menuList, MenuResp.class); + list.forEach(super::fill); + return list; + } + + /** + * 标题是否存在 + * + * @param title 标题 + * @param parentId 上级 ID + * @param id ID + * @return true:存在;false:不存在 + */ + private boolean isTitleExists(String title, Long parentId, Long id) { + return baseMapper.lambdaQuery() + .eq(MenuDO::getTitle, title) + .eq(MenuDO::getParentId, parentId) + .ne(null != id, MenuDO::getId, id) + .exists(); + } + + /** + * 名称是否存在 + * + * @param name 标题 + * @param id ID + * @return true:存在;false:不存在 + */ + private boolean isNameExists(String name, Long id) { + return baseMapper.lambdaQuery() + .eq(MenuDO::getName, name) + .ne(MenuDO::getType, MenuTypeEnum.BUTTON) + .ne(null != id, MenuDO::getId, id) + .exists(); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/OptionServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/OptionServiceImpl.java new file mode 100644 index 0000000..cf6e61b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/OptionServiceImpl.java @@ -0,0 +1,125 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alicp.jetcache.anno.Cached; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.enums.OptionCategoryEnum; +import top.wms.admin.system.enums.PasswordPolicyEnum; +import top.wms.admin.system.mapper.OptionMapper; +import top.wms.admin.system.model.entity.OptionDO; +import top.wms.admin.system.model.query.OptionQuery; +import top.wms.admin.system.model.req.OptionReq; +import top.wms.admin.system.model.req.OptionResetValueReq; +import top.wms.admin.system.model.resp.OptionResp; +import top.wms.admin.system.service.OptionService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.data.mp.util.QueryWrapperHelper; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 参数业务实现 + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +@Service +@RequiredArgsConstructor +public class OptionServiceImpl implements OptionService { + + private final OptionMapper baseMapper; + + @Override + public List list(OptionQuery query) { + return BeanUtil.copyToList(baseMapper.selectList(QueryWrapperHelper.build(query)), OptionResp.class); + } + + @Override + @Cached(key = "#category", name = CacheConstants.OPTION_KEY_PREFIX + "MAP:") + public Map getByCategory(OptionCategoryEnum category) { + return baseMapper.selectByCategory(category.name()) + .stream() + .collect(Collectors.toMap(OptionDO::getCode, o -> StrUtil.emptyIfNull(ObjectUtil.defaultIfNull(o + .getValue(), o.getDefaultValue())), (oldVal, newVal) -> oldVal)); + } + + @Override + public void update(List options) { + // 非空校验 + List idList = options.stream().map(OptionReq::getId).toList(); + List optionList = baseMapper.selectByIds(idList); + Map optionMap = optionList.stream() + .collect(Collectors.toMap(OptionDO::getCode, Function.identity(), (existing, replacement) -> existing)); + for (OptionReq req : options) { + OptionDO option = optionMap.get(req.getCode()); + ValidationUtils.throwIfNull(option, "参数 [{}] 不存在", req.getCode()); + if (StrUtil.isNotBlank(option.getDefaultValue())) { + ValidationUtils.throwIfBlank(req.getValue(), "参数 [{}] 的值不能为空", option.getName()); + } + } + // 校验密码策略参数取值范围 + Map passwordPolicyOptionMap = options.stream() + .filter(option -> StrUtil.startWith(option.getCode(), PasswordPolicyEnum.CATEGORY + .name() + StringConstants.UNDERLINE)) + .collect(Collectors.toMap(OptionReq::getCode, OptionReq::getValue, (oldVal, newVal) -> oldVal)); + for (Map.Entry passwordPolicyOptionEntry : passwordPolicyOptionMap.entrySet()) { + String code = passwordPolicyOptionEntry.getKey(); + String value = passwordPolicyOptionEntry.getValue(); + ValidationUtils.throwIf(!NumberUtil.isNumber(value), "参数 [%s] 的值必须为数字", code); + PasswordPolicyEnum passwordPolicy = PasswordPolicyEnum.valueOf(code); + passwordPolicy.validateRange(Integer.parseInt(value), passwordPolicyOptionMap); + } + RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK); + baseMapper.updateById(BeanUtil.copyToList(options, OptionDO.class)); + } + + @Override + public void resetValue(OptionResetValueReq req) { + RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK); + String category = req.getCategory(); + List codeList = req.getCode(); + ValidationUtils.throwIf(StrUtil.isBlank(category) && CollUtil.isEmpty(codeList), "键列表不能为空"); + LambdaUpdateChainWrapper updateWrapper = baseMapper.lambdaUpdate().set(OptionDO::getValue, null); + if (StrUtil.isNotBlank(category)) { + updateWrapper.eq(OptionDO::getCategory, category); + } else { + updateWrapper.in(OptionDO::getCode, req.getCode()); + } + updateWrapper.update(); + } + + @Override + public int getValueByCode2Int(String code) { + return this.getValueByCode(code, Integer::parseInt); + } + + @Override + public T getValueByCode(String code, Function mapper) { + String value = RedisUtils.get(CacheConstants.OPTION_KEY_PREFIX + code); + if (StrUtil.isNotBlank(value)) { + return mapper.apply(value); + } + OptionDO option = baseMapper.lambdaQuery() + .eq(OptionDO::getCode, code) + .select(OptionDO::getValue, OptionDO::getDefaultValue) + .one(); + CheckUtils.throwIfNull(option, "参数 [{}] 不存在", code); + value = StrUtil.nullToDefault(option.getValue(), option.getDefaultValue()); + CheckUtils.throwIfBlank(value, "参数 [{}] 数据错误", code); + RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value); + return mapper.apply(value); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleDeptServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleDeptServiceImpl.java new file mode 100644 index 0000000..e91d011 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleDeptServiceImpl.java @@ -0,0 +1,68 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.system.mapper.RoleDeptMapper; +import top.wms.admin.system.model.entity.RoleDeptDO; +import top.wms.admin.system.service.RoleDeptService; + +import java.util.List; + +/** + * 角色和部门业务实现 + * + * @author Charles7c + * @since 2023/2/19 10:47 + */ +@Service +@RequiredArgsConstructor +public class RoleDeptServiceImpl implements RoleDeptService { + + private final RoleDeptMapper baseMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean add(List deptIds, Long roleId) { + // 检查是否有变更 + List oldDeptIdList = baseMapper.lambdaQuery() + .select(RoleDeptDO::getDeptId) + .eq(RoleDeptDO::getRoleId, roleId) + .list() + .stream() + .map(RoleDeptDO::getDeptId) + .toList(); + if (CollUtil.isEmpty(CollUtil.disjunction(deptIds, oldDeptIdList))) { + return false; + } + // 删除原有关联 + baseMapper.lambdaUpdate().eq(RoleDeptDO::getRoleId, roleId).remove(); + // 保存最新关联 + List roleDeptList = deptIds.stream().map(deptId -> new RoleDeptDO(roleId, deptId)).toList(); + return baseMapper.insertBatch(roleDeptList); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByRoleIds(List roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return; + } + baseMapper.lambdaUpdate().in(RoleDeptDO::getRoleId, roleIds).remove(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByDeptIds(List deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return; + } + baseMapper.lambdaUpdate().in(RoleDeptDO::getDeptId, deptIds).remove(); + } + + @Override + public List listDeptIdByRoleId(Long roleId) { + return baseMapper.selectDeptIdByRoleId(roleId); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleMenuServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleMenuServiceImpl.java new file mode 100644 index 0000000..ed70f84 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleMenuServiceImpl.java @@ -0,0 +1,64 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.system.mapper.RoleMenuMapper; +import top.wms.admin.system.model.entity.RoleMenuDO; +import top.wms.admin.system.service.RoleMenuService; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 角色和菜单业务实现 + * + * @author Charles7c + * @since 2023/2/19 10:43 + */ +@Service +@RequiredArgsConstructor +public class RoleMenuServiceImpl implements RoleMenuService { + + private final RoleMenuMapper baseMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean add(List menuIds, Long roleId) { + // 检查是否有变更 + List oldMenuIdList = baseMapper.lambdaQuery() + .select(RoleMenuDO::getMenuId) + .eq(RoleMenuDO::getRoleId, roleId) + .list() + .stream() + .map(RoleMenuDO::getMenuId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(CollUtil.disjunction(menuIds, oldMenuIdList))) { + return false; + } + // 删除原有关联 + baseMapper.lambdaUpdate().eq(RoleMenuDO::getRoleId, roleId).remove(); + // 保存最新关联 + List roleMenuList = menuIds.stream().map(menuId -> new RoleMenuDO(roleId, menuId)).toList(); + return baseMapper.insertBatch(roleMenuList); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByRoleIds(List roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return; + } + baseMapper.lambdaUpdate().in(RoleMenuDO::getRoleId, roleIds).remove(); + } + + @Override + public List listMenuIdByRoleIds(List roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return new ArrayList<>(0); + } + return baseMapper.selectMenuIdByRoleIds(roleIds); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..231160f --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/RoleServiceImpl.java @@ -0,0 +1,247 @@ +package top.wms.admin.system.service.impl; + +import cn.crane4j.annotation.ContainerMethod; +import cn.crane4j.annotation.MappingType; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alicp.jetcache.anno.CacheInvalidate; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.common.context.RoleContext; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.enums.DataScopeEnum; +import top.wms.admin.system.mapper.RoleMapper; +import top.wms.admin.system.model.entity.RoleDO; +import top.wms.admin.system.model.query.RoleQuery; +import top.wms.admin.system.model.req.RoleReq; +import top.wms.admin.system.model.req.RoleUpdatePermissionReq; +import top.wms.admin.system.model.resp.MenuResp; +import top.wms.admin.system.model.resp.role.RoleDetailResp; +import top.wms.admin.system.model.resp.role.RoleResp; +import top.wms.admin.system.service.*; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 角色业务实现 + * + * @author Charles7c + * @since 2023/2/8 23:17 + */ +@Service +@RequiredArgsConstructor +public class RoleServiceImpl extends BaseServiceImpl implements RoleService { + + private final MenuService menuService; + private final RoleMenuService roleMenuService; + private final RoleDeptService roleDeptService; + private final UserRoleService userRoleService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long add(RoleReq req) { + String name = req.getName(); + CheckUtils.throwIf(this.isNameExists(name, null), "新增失败,[{}] 已存在", name); + String code = req.getCode(); + CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); + // 新增信息 + Long roleId = super.add(req); + // 保存角色和部门关联 + roleDeptService.add(req.getDeptIds(), roleId); + return roleId; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(RoleReq req, Long id) { + String name = req.getName(); + CheckUtils.throwIf(this.isNameExists(name, id), "修改失败,[{}] 已存在", name); + RoleDO oldRole = super.getById(id); + CheckUtils.throwIfNotEqual(req.getCode(), oldRole.getCode(), "角色编码不允许修改", oldRole.getName()); + DataScopeEnum oldDataScope = oldRole.getDataScope(); + if (Boolean.TRUE.equals(oldRole.getIsSystem())) { + CheckUtils.throwIfNotEqual(req.getDataScope(), oldDataScope, "[{}] 是系统内置角色,不允许修改角色数据权限", oldRole.getName()); + } + // 更新信息 + super.update(req, id); + if (SysConstants.SUPER_ROLE_CODE.equals(req.getCode())) { + return; + } + // 保存角色和部门关联 + boolean isSaveDeptSuccess = roleDeptService.add(req.getDeptIds(), id); + // 如果数据权限有变更,则更新在线用户权限信息 + if (isSaveDeptSuccess || ObjectUtil.notEqual(req.getDataScope(), oldDataScope)) { + this.updateUserContext(id); + } + } + + @Override + public void beforeDelete(List ids) { + List list = baseMapper.lambdaQuery() + .select(RoleDO::getName, RoleDO::getIsSystem) + .in(RoleDO::getId, ids) + .list(); + Optional isSystemData = list.stream().filter(RoleDO::getIsSystem).findFirst(); + CheckUtils.throwIf(isSystemData::isPresent, "所选角色 [{}] 是系统内置角色,不允许删除", isSystemData.orElseGet(RoleDO::new) + .getName()); + CheckUtils.throwIf(userRoleService.isRoleIdExists(ids), "所选角色存在用户关联,请解除关联后重试"); + // 删除角色和菜单关联 + roleMenuService.deleteByRoleIds(ids); + // 删除角色和部门关联 + roleDeptService.deleteByRoleIds(ids); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheInvalidate(key = "#id", name = CacheConstants.ROLE_MENU_KEY_PREFIX) + public void updatePermission(Long id, RoleUpdatePermissionReq req) { + super.getById(id); + // 保存角色和菜单关联 + boolean isSaveMenuSuccess = roleMenuService.add(req.getMenuIds(), id); + // 如果功能权限有变更,则更新在线用户权限信息 + if (isSaveMenuSuccess) { + this.updateUserContext(id); + } + baseMapper.lambdaUpdate() + .set(RoleDO::getMenuCheckStrictly, req.getMenuCheckStrictly()) + .eq(RoleDO::getId, id) + .update(); + } + + @Override + public void assignToUsers(Long id, List userIds) { + super.getById(id); + // 保存用户和角色关联 + userRoleService.assignRoleToUsers(id, userIds); + // 更新用户上下文 + this.updateUserContext(id); + } + + @Override + public void fill(Object obj) { + super.fill(obj); + if (obj instanceof RoleDetailResp detail) { + Long roleId = detail.getId(); + List list = menuService.listByRoleId(roleId); + List menuIds = list.stream().map(MenuResp::getId).toList(); + detail.setMenuIds(menuIds); + } + } + + @Override + public Set listPermissionByUserId(Long userId) { + Set roleCodeSet = this.listCodeByUserId(userId); + // 超级管理员赋予全部权限 + if (roleCodeSet.contains(SysConstants.SUPER_ROLE_CODE)) { + return CollUtil.newHashSet(SysConstants.ALL_PERMISSION); + } + return menuService.listPermissionByUserId(userId); + } + + @Override + @ContainerMethod(namespace = ContainerConstants.USER_ROLE_NAME_LIST, type = MappingType.ORDER_OF_KEYS) + public List listNameByIds(List ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List roleList = baseMapper.lambdaQuery().select(RoleDO::getName).in(RoleDO::getId, ids).list(); + return roleList.stream().map(RoleDO::getName).toList(); + } + + @Override + public Set listCodeByUserId(Long userId) { + List roleIdList = userRoleService.listRoleIdByUserId(userId); + if (CollUtil.isEmpty(roleIdList)) { + return Collections.emptySet(); + } + List roleList = baseMapper.lambdaQuery().select(RoleDO::getCode).in(RoleDO::getId, roleIdList).list(); + return roleList.stream().map(RoleDO::getCode).collect(Collectors.toSet()); + } + + @Override + public Set listByUserId(Long userId) { + List roleIdList = userRoleService.listRoleIdByUserId(userId); + if (CollUtil.isEmpty(roleIdList)) { + return Collections.emptySet(); + } + List roleList = baseMapper.lambdaQuery() + .select(RoleDO::getId, RoleDO::getCode, RoleDO::getDataScope) + .in(RoleDO::getId, roleIdList) + .list(); + return roleList.stream() + .map(r -> new RoleContext(r.getId(), r.getCode(), r.getDataScope())) + .collect(Collectors.toSet()); + } + + @Override + public RoleDO getByCode(String code) { + return baseMapper.lambdaQuery().eq(RoleDO::getCode, code).one(); + } + + @Override + public List listByNames(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + return this.list(Wrappers.lambdaQuery().in(RoleDO::getName, list)); + } + + @Override + public int countByNames(List roleNames) { + if (CollUtil.isEmpty(roleNames)) { + return 0; + } + return (int)this.count(Wrappers.lambdaQuery().in(RoleDO::getName, roleNames)); + } + + /** + * 名称是否存在 + * + * @param name 名称 + * @param id ID + * @return 是否存在 + */ + private boolean isNameExists(String name, Long id) { + return baseMapper.lambdaQuery().eq(RoleDO::getName, name).ne(null != id, RoleDO::getId, id).exists(); + } + + /** + * 编码是否存在 + * + * @param code 编码 + * @param id ID + * @return 是否存在 + */ + private boolean isCodeExists(String code, Long id) { + return baseMapper.lambdaQuery().eq(RoleDO::getCode, code).ne(null != id, RoleDO::getId, id).exists(); + } + + /** + * 更新用户上下文 + * + * @param roleId 角色 ID + */ + private void updateUserContext(Long roleId) { + List userIdList = userRoleService.listUserIdByRoleId(roleId); + userIdList.parallelStream().forEach(userId -> { + UserContext userContext = UserContextHolder.getContext(userId); + if (null != userContext) { + userContext.setRoles(this.listByUserId(userId)); + userContext.setPermissions(this.listPermissionByUserId(userId)); + UserContextHolder.setContext(userContext); + } + }); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/StorageServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/StorageServiceImpl.java new file mode 100644 index 0000000..647f2fb --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/StorageServiceImpl.java @@ -0,0 +1,231 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.dromara.x.file.storage.core.FileStorageProperties; +import org.dromara.x.file.storage.core.FileStorageService; +import org.dromara.x.file.storage.core.FileStorageServiceBuilder; +import org.dromara.x.file.storage.core.platform.FileStorage; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.model.req.CommonStatusUpdateReq; +import top.wms.admin.common.util.SecureUtils; +import top.wms.admin.system.enums.StorageTypeEnum; +import top.wms.admin.system.mapper.StorageMapper; +import top.wms.admin.system.model.entity.StorageDO; +import top.wms.admin.system.model.query.StorageQuery; +import top.wms.admin.system.model.req.StorageReq; +import top.wms.admin.system.model.resp.StorageResp; +import top.wms.admin.system.service.FileService; +import top.wms.admin.system.service.StorageService; +import top.wms.admin.system.validation.ValidationGroup; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.ExceptionUtils; +import top.continew.starter.core.util.URLUtils; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.extension.crud.service.BaseServiceImpl; +import top.continew.starter.web.util.SpringWebUtils; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 存储业务实现 + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +@Service +@RequiredArgsConstructor +public class StorageServiceImpl extends BaseServiceImpl implements StorageService { + + private final FileStorageService fileStorageService; + @Resource + private FileService fileService; + + @Override + public void beforeAdd(StorageReq req) { + this.decodeSecretKey(req, null); + String code = req.getCode(); + CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); + // 单独指定默认存储 + req.setIsDefault(false); + if (DisEnableStatusEnum.ENABLE.equals(req.getStatus())) { + this.load(req); + } + } + + @Override + public void beforeUpdate(StorageReq req, Long id) { + StorageDO oldStorage = super.getById(id); + CheckUtils.throwIfNotEqual(req.getCode(), oldStorage.getCode(), "不允许修改存储编码"); + CheckUtils.throwIfNotEqual(req.getType(), oldStorage.getType(), "不允许修改存储类型"); + this.decodeSecretKey(req, oldStorage); + DisEnableStatusEnum newStatus = req.getStatus(); + CheckUtils.throwIf(Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE + .equals(newStatus), "[{}] 是默认存储,不允许禁用", oldStorage.getName()); + // 重新加载存储引擎 + DisEnableStatusEnum oldStatus = oldStorage.getStatus(); + // 先卸载 + if (DisEnableStatusEnum.ENABLE.equals(oldStatus)) { + this.unload(BeanUtil.copyProperties(oldStorage, StorageReq.class)); + } + // 再加载 + if (DisEnableStatusEnum.ENABLE.equals(newStatus)) { + this.load(req); + } + } + + @Override + public void beforeDelete(List ids) { + CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试"); + List storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list(); + storageList.forEach(s -> { + CheckUtils.throwIfEqual(Boolean.TRUE, s.getIsDefault(), "[{}] 是默认存储,不允许删除", s.getName()); + // 卸载启用状态的存储 + if (DisEnableStatusEnum.ENABLE.equals(s.getStatus())) { + this.unload(BeanUtil.copyProperties(s, StorageReq.class)); + } + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStatus(CommonStatusUpdateReq req, Long id) { + StorageDO storage = super.getById(id); + // 状态未改变 + DisEnableStatusEnum newStatus = req.getStatus(); + if (storage.getStatus().equals(newStatus)) { + return; + } + // 修改状态 + baseMapper.lambdaUpdate().eq(StorageDO::getId, id).set(StorageDO::getStatus, newStatus).update(); + // 加载、卸载存储引擎 + StorageReq storageReq = BeanUtil.copyProperties(storage, StorageReq.class); + switch (newStatus) { + case ENABLE: + this.load(storageReq); + break; + case DISABLE: + CheckUtils.throwIfEqual(Boolean.TRUE, storage.getIsDefault(), "[{}] 是默认存储,不允许禁用", storage.getName()); + this.unload(storageReq); + break; + default: + break; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void setDefault(Long id) { + StorageDO storage = super.getById(id); + if (Boolean.TRUE.equals(storage.getIsDefault())) { + return; + } + baseMapper.lambdaUpdate().eq(StorageDO::getIsDefault, true).set(StorageDO::getIsDefault, false).update(); + baseMapper.lambdaUpdate().eq(StorageDO::getId, id).set(StorageDO::getIsDefault, true).update(); + } + + @Override + public StorageDO getDefaultStorage() { + return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).one(); + } + + @Override + public StorageDO getByCode(String code) { + return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one(); + } + + @Override + public void load(StorageReq req) { + CopyOnWriteArrayList fileStorageList = fileStorageService.getFileStorageList(); + String domain = req.getDomain(); + ValidationUtils.throwIf(!URLUtils.isHttpUrl(domain), "域名格式错误"); + String bucketName = req.getBucketName(); + StorageTypeEnum type = req.getType(); + if (StorageTypeEnum.LOCAL.equals(type)) { + ValidationUtils.validate(req, ValidationGroup.Storage.Local.class); + req.setBucketName(StrUtil.appendIfMissing(bucketName + .replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH)); + FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig(); + config.setPlatform(req.getCode()); + config.setStoragePath(bucketName); + fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections + .singletonList(config))); + SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), bucketName)); + } else if (StorageTypeEnum.OSS.equals(type)) { + ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class); + FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config(); + config.setPlatform(req.getCode()); + config.setAccessKey(req.getAccessKey()); + config.setSecretKey(req.getSecretKey()); + config.setEndPoint(req.getEndpoint()); + config.setBucketName(bucketName); + config.setDomain(domain); + fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections + .singletonList(config), null)); + } + } + + @Override + public void unload(StorageReq req) { + CopyOnWriteArrayList fileStorageList = fileStorageService.getFileStorageList(); + FileStorage fileStorage = fileStorageService.getFileStorage(req.getCode()); + fileStorageList.remove(fileStorage); + fileStorage.close(); + SpringWebUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), req + .getBucketName())); + } + + /** + * 解密 SecretKey + * + * @param req 请求参数 + * @param storage 存储信息 + */ + private void decodeSecretKey(StorageReq req, StorageDO storage) { + if (!StorageTypeEnum.OSS.equals(req.getType())) { + return; + } + // 修改时,如果 SecretKey 不修改,需要手动修正 + String newSecretKey = req.getSecretKey(); + boolean isSecretKeyNotUpdate = StrUtil.isBlank(newSecretKey) || newSecretKey.contains(StringConstants.ASTERISK); + if (null != storage && isSecretKeyNotUpdate) { + req.setSecretKey(storage.getSecretKey()); + return; + } + // 新增时或修改了 SecretKey + String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(newSecretKey)); + ValidationUtils.throwIfNull(secretKey, "私有密钥解密失败"); + ValidationUtils.throwIf(secretKey.length() > 255, "私有密钥长度不能超过 255 个字符"); + req.setSecretKey(secretKey); + } + + /** + * 默认存储是否存在 + * + * @param id ID + * @return 是否存在 + */ + private boolean isDefaultExists(Long id) { + return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).ne(null != id, StorageDO::getId, id).exists(); + } + + /** + * 编码是否存在 + * + * @param code 编码 + * @param id ID + * @return 是否存在 + */ + private boolean isCodeExists(String code, Long id) { + return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).ne(null != id, StorageDO::getId, id).exists(); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserPasswordHistoryServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserPasswordHistoryServiceImpl.java new file mode 100644 index 0000000..eb9ab0b --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserPasswordHistoryServiceImpl.java @@ -0,0 +1,63 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.system.mapper.UserPasswordHistoryMapper; +import top.wms.admin.system.model.entity.UserPasswordHistoryDO; +import top.wms.admin.system.service.UserPasswordHistoryService; + +import java.util.List; + +/** + * 用户历史密码业务实现 + * + * @author Charles7c + * @since 2024/5/16 21:58 + */ +@Service +@RequiredArgsConstructor +public class UserPasswordHistoryServiceImpl implements UserPasswordHistoryService { + + private final UserPasswordHistoryMapper baseMapper; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(Long userId, String password, int count) { + if (StrUtil.isBlank(password)) { + return; + } + baseMapper.insert(new UserPasswordHistoryDO(userId, password)); + // 删除过期历史密码 + baseMapper.deleteExpired(userId, count); + } + + @Override + public void deleteByUserIds(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return; + } + baseMapper.lambdaUpdate().in(UserPasswordHistoryDO::getUserId, userIds).remove(); + } + + @Override + public boolean isPasswordReused(Long userId, String password, int count) { + // 查询近 N 个历史密码 + List list = baseMapper.lambdaQuery() + .select(UserPasswordHistoryDO::getPassword) + .eq(UserPasswordHistoryDO::getUserId, userId) + .orderByDesc(UserPasswordHistoryDO::getCreateTime) + .last("LIMIT %s".formatted(count)) + .list(); + if (CollUtil.isEmpty(list)) { + return false; + } + // 校验是否重复使用历史密码 + List passwordList = list.stream().map(UserPasswordHistoryDO::getPassword).toList(); + return passwordList.stream().anyMatch(p -> passwordEncoder.matches(password, p)); + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserRoleServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserRoleServiceImpl.java new file mode 100644 index 0000000..a692fcd --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserRoleServiceImpl.java @@ -0,0 +1,133 @@ +package top.wms.admin.system.service.impl; + +import cn.crane4j.annotation.AutoOperate; +import cn.crane4j.annotation.ContainerMethod; +import cn.crane4j.annotation.MappingType; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +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.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.system.mapper.UserRoleMapper; +import top.wms.admin.system.model.entity.UserRoleDO; +import top.wms.admin.system.model.query.RoleUserQuery; +import top.wms.admin.system.model.resp.role.RoleUserResp; +import top.wms.admin.system.service.UserRoleService; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.data.mp.util.QueryWrapperHelper; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; + +/** + * 用户和角色业务实现 + * + * @author Charles7c + * @since 2023/2/20 21:30 + */ +@Service +@RequiredArgsConstructor +public class UserRoleServiceImpl implements UserRoleService { + + private final UserRoleMapper baseMapper; + + @Override + @AutoOperate(type = RoleUserResp.class, on = "list") + public PageResp pageUser(RoleUserQuery query, PageQuery pageQuery) { + String description = query.getDescription(); + QueryWrapper queryWrapper = new QueryWrapper().eq("t1.role_id", query.getRoleId()) + .and(StrUtil.isNotBlank(description), q -> q.like("t2.username", description) + .or() + .like("t2.nickname", description) + .or() + .like("t2.description", description)); + QueryWrapperHelper.sort(queryWrapper, pageQuery.getSort()); + IPage page = baseMapper.selectUserPage(new Page<>(pageQuery.getPage(), pageQuery + .getSize()), queryWrapper); + return PageResp.build(page); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean assignRolesToUser(List roleIds, Long userId) { + // 检查是否有变更 + List oldRoleIdList = baseMapper.lambdaQuery() + .select(UserRoleDO::getRoleId) + .eq(UserRoleDO::getUserId, userId) + .list() + .stream() + .map(UserRoleDO::getRoleId) + .toList(); + if (CollUtil.isEmpty(CollUtil.disjunction(roleIds, oldRoleIdList))) { + return false; + } + CheckUtils.throwIf(SysConstants.SUPER_USER_ID.equals(userId) && !roleIds + .contains(SysConstants.SUPER_ROLE_ID), "不允许变更超管用户角色"); + // 删除原有关联 + baseMapper.lambdaUpdate().eq(UserRoleDO::getUserId, userId).remove(); + // 保存最新关联 + List userRoleList = roleIds.stream().map(roleId -> new UserRoleDO(userId, roleId)).toList(); + return baseMapper.insertBatch(userRoleList); + } + + @Override + public boolean assignRoleToUsers(Long roleId, List userIds) { + List userRoleList = userIds.stream().map(userId -> new UserRoleDO(userId, roleId)).toList(); + return baseMapper.insertBatch(userRoleList); + } + + @Override + public void deleteByIds(List ids) { + baseMapper.deleteByIds(ids); + } + + @Override + public void deleteByUserIds(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return; + } + baseMapper.lambdaUpdate().in(UserRoleDO::getUserId, userIds).remove(); + } + + @Override + public void saveBatch(List list) { + baseMapper.insert(list); + } + + @Override + @ContainerMethod(namespace = ContainerConstants.USER_ROLE_ID_LIST, type = MappingType.ORDER_OF_KEYS) + public List listRoleIdByUserId(Long userId) { + return baseMapper.lambdaQuery() + .select(UserRoleDO::getRoleId) + .eq(UserRoleDO::getUserId, userId) + .list() + .stream() + .map(UserRoleDO::getRoleId) + .toList(); + } + + @Override + public List listUserIdByRoleId(Long roleId) { + return baseMapper.lambdaQuery() + .select(UserRoleDO::getUserId) + .eq(UserRoleDO::getRoleId, roleId) + .list() + .stream() + .map(UserRoleDO::getUserId) + .toList(); + } + + @Override + public boolean isRoleIdExists(List roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return false; + } + return baseMapper.lambdaQuery().in(UserRoleDO::getRoleId, roleIds).exists(); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..fcef782 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserServiceImpl.java @@ -0,0 +1,744 @@ +package top.wms.admin.system.service.impl; + +import cn.crane4j.annotation.ContainerMethod; +import cn.crane4j.annotation.MappingType; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.UUID; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.validation.ValidationUtil; +import cn.hutool.http.ContentType; +import cn.hutool.json.JSONUtil; +import com.alibaba.excel.EasyExcel; +import com.alicp.jetcache.anno.CacheInvalidate; +import com.alicp.jetcache.anno.CacheType; +import com.alicp.jetcache.anno.CacheUpdate; +import com.alicp.jetcache.anno.Cached; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.ahoo.cosid.IdGenerator; +import me.ahoo.cosid.provider.DefaultIdGeneratorProvider; +import net.dreamlu.mica.core.result.R; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.auth.service.OnlineUserService; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.constant.ContainerConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.enums.DisEnableStatusEnum; +import top.wms.admin.common.enums.GenderEnum; +import top.wms.admin.common.service.CommonUserService; +import top.wms.admin.common.util.SecureUtils; +import top.wms.admin.system.enums.OptionCategoryEnum; +import top.wms.admin.system.mapper.UserMapper; +import top.wms.admin.system.model.entity.DeptDO; +import top.wms.admin.system.model.entity.RoleDO; +import top.wms.admin.system.model.entity.UserDO; +import top.wms.admin.system.model.entity.UserRoleDO; +import top.wms.admin.system.model.query.UserQuery; +import top.wms.admin.system.model.req.user.*; +import top.wms.admin.system.model.resp.user.UserDetailResp; +import top.wms.admin.system.model.resp.user.UserImportParseResp; +import top.wms.admin.system.model.resp.user.UserImportResp; +import top.wms.admin.system.model.resp.user.UserResp; +import top.wms.admin.system.service.*; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.core.util.SpringUtils; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.query.SortQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; +import top.continew.starter.extension.crud.service.BaseServiceImpl; +import top.continew.starter.web.util.FileUploadUtils; + +import java.io.IOException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static top.wms.admin.system.enums.ImportPolicyEnum.*; +import static top.wms.admin.system.enums.PasswordPolicyEnum.*; + +/** + * 用户业务实现 + * + * @author Charles7c + * @since 2022/12/21 21:49 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl extends BaseServiceImpl implements UserService, CommonUserService { + + private final PasswordEncoder passwordEncoder; + private final UserPasswordHistoryService userPasswordHistoryService; + private final OnlineUserService onlineUserService; + private final OptionService optionService; + private final UserRoleService userRoleService; + private final RoleService roleService; + private final FileService fileService; + private final FileStorageService fileStorageService; + + @Resource + private DeptService deptService; + @Value("${avatar.support-suffix}") + private String[] avatarSupportSuffix; + @Value("${avatar.path}") + private String avatarPath; + + @Override + public PageResp page(UserQuery query, PageQuery pageQuery) { + QueryWrapper queryWrapper = this.buildQueryWrapper(query); + super.sort(queryWrapper, pageQuery); + IPage page = baseMapper.selectUserPage(new Page<>(pageQuery.getPage(), pageQuery + .getSize()), queryWrapper); + PageResp pageResp = PageResp.build(page, super.getListClass()); + pageResp.getList().forEach(this::fill); + return pageResp; + } + + @Override + public void beforeAdd(UserReq req) { + final String errorMsgTemplate = "新增失败,[{}] 已存在"; + String username = req.getUsername(); + CheckUtils.throwIf(this.isNameExists(username, null), errorMsgTemplate, username); + String email = req.getEmail(); + CheckUtils.throwIf(StrUtil.isNotBlank(email) && this.isEmailExists(email, null), errorMsgTemplate, email); + String phone = req.getPhone(); + CheckUtils.throwIf(StrUtil.isNotBlank(phone) && this.isPhoneExists(phone, null), errorMsgTemplate, phone); + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + req.setPassword("{bcrypt}" + encoder.encode(req.getPassword())); + } + + @Override + public void afterAdd(UserReq req, UserDO user) { + Long userId = user.getId(); + baseMapper.lambdaUpdate().set(UserDO::getPwdResetTime, LocalDateTime.now()).eq(UserDO::getId, userId).update(); + // 保存用户和角色关联 + userRoleService.assignRolesToUser(req.getRoleIds(), userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheUpdate(key = "#id", value = "#req.nickname", name = CacheConstants.USER_KEY_PREFIX) + public void update(UserReq req, Long id) { + final String errorMsgTemplate = "修改失败,[{}] 已存在"; + String username = req.getUsername(); + CheckUtils.throwIf(this.isNameExists(username, id), errorMsgTemplate, username); + String email = req.getEmail(); + CheckUtils.throwIf(StrUtil.isNotBlank(email) && this.isEmailExists(email, id), errorMsgTemplate, email); + String phone = req.getPhone(); + CheckUtils.throwIf(StrUtil.isNotBlank(phone) && this.isPhoneExists(phone, id), errorMsgTemplate, phone); + DisEnableStatusEnum newStatus = req.getStatus(); + CheckUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(newStatus) && ObjectUtil.equal(id, UserContextHolder + .getUserId()), "不允许禁用当前用户"); + UserDO oldUser = super.getById(id); + if (Boolean.TRUE.equals(oldUser.getIsSystem())) { + CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, newStatus, "[{}] 是系统内置用户,不允许禁用", oldUser + .getUsername()); + Collection disjunctionRoleIds = CollUtil.disjunction(req.getRoleIds(), userRoleService + .listRoleIdByUserId(id)); + CheckUtils.throwIfNotEmpty(disjunctionRoleIds, "[{}] 是系统内置用户,不允许变更角色", oldUser.getUsername()); + } + // 更新信息 + UserDO newUser = BeanUtil.toBean(req, UserDO.class); + newUser.setId(id); + baseMapper.updateById(newUser); + // 保存用户和角色关联 + boolean isSaveUserRoleSuccess = userRoleService.assignRolesToUser(req.getRoleIds(), id); + // 如果禁用用户,则踢出在线用户 + if (DisEnableStatusEnum.DISABLE.equals(newStatus)) { + onlineUserService.kickOut(id); + return; + } + // 如果角色有变更,则更新在线用户权限信息 + if (isSaveUserRoleSuccess) { + this.updateContext(id); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheInvalidate(key = "#ids", name = CacheConstants.USER_KEY_PREFIX, multi = true) + public void delete(List ids) { + CheckUtils.throwIf(CollUtil.contains(ids, UserContextHolder.getUserId()), "不允许删除当前用户"); + List list = baseMapper.lambdaQuery() + .select(UserDO::getUsername, UserDO::getIsSystem) + .in(UserDO::getId, ids) + .list(); + Optional isSystemData = list.stream().filter(UserDO::getIsSystem).findFirst(); + CheckUtils.throwIf(isSystemData::isPresent, "所选用户 [{}] 是系统内置用户,不允许删除", isSystemData.orElseGet(UserDO::new) + .getUsername()); + // 删除用户和角色关联 + userRoleService.deleteByUserIds(ids); + // 删除历史密码 + userPasswordHistoryService.deleteByUserIds(ids); + // 删除用户 + super.delete(ids); + // 踢出在线用户 + ids.forEach(onlineUserService::kickOut); + } + + @Override + @Cached(key = "#id", name = CacheConstants.USER_KEY_PREFIX, cacheType = CacheType.BOTH, syncLocal = true) + public String getNicknameById(Long id) { + return baseMapper.selectNicknameById(id); + } + + @Override + public void downloadImportTemplate(HttpServletResponse response) throws IOException { + try { + FileUploadUtils.download(response, ResourceUtil.getStream("templates/import/user.xlsx"), "用户导入模板.xlsx"); + } catch (Exception e) { + log.error("下载用户导入模板失败:{}", e.getMessage(), e); + response.setCharacterEncoding(CharsetUtil.UTF_8); + response.setContentType(ContentType.JSON.toString()); + response.getWriter().write(JSONUtil.toJsonStr(R.fail("下载用户导入模板失败"))); + } + } + + @Override + public UserImportParseResp parseImport(MultipartFile file) { + UserImportParseResp userImportResp = new UserImportParseResp(); + List importRowList; + // 读取表格数据 + try { + importRowList = EasyExcel.read(file.getInputStream()) + .head(UserImportRowReq.class) + .sheet() + .headRowNumber(1) + .doReadSync(); + } catch (Exception e) { + log.error("用户导入数据文件解析异常:{}", e.getMessage(), e); + throw new BusinessException("数据文件解析异常"); + } + // 总计行数 + userImportResp.setTotalRows(importRowList.size()); + CheckUtils.throwIfEmpty(importRowList, "数据文件格式错误"); + // 有效行数:过滤无效数据 + List validRowList = this.filterImportData(importRowList); + userImportResp.setValidRows(validRowList.size()); + CheckUtils.throwIfEmpty(validRowList, "数据文件格式错误"); + + // 检测表格内数据是否合法 + Set seenEmails = new HashSet<>(); + boolean hasDuplicateEmail = validRowList.stream() + .map(UserImportRowReq::getEmail) + .anyMatch(email -> email != null && !seenEmails.add(email)); + CheckUtils.throwIf(hasDuplicateEmail, "存在重复邮箱,请检测数据"); + Set seenPhones = new HashSet<>(); + boolean hasDuplicatePhone = validRowList.stream() + .map(UserImportRowReq::getPhone) + .anyMatch(phone -> phone != null && !seenPhones.add(phone)); + CheckUtils.throwIf(hasDuplicatePhone, "存在重复手机,请检测数据"); + + // 校验是否存在错误角色 + List roleNames = validRowList.stream().map(UserImportRowReq::getRoleName).distinct().toList(); + int existRoleCount = roleService.countByNames(roleNames); + CheckUtils.throwIf(existRoleCount < roleNames.size(), "存在错误角色,请检查数据"); + // 校验是否存在错误部门 + List deptNames = validRowList.stream().map(UserImportRowReq::getDeptName).distinct().toList(); + int existDeptCount = deptService.countByNames(deptNames); + CheckUtils.throwIf(existDeptCount < deptNames.size(), "存在错误部门,请检查数据"); + + // 查询重复用户 + userImportResp + .setDuplicateUserRows(countExistByField(validRowList, UserImportRowReq::getUsername, UserDO::getUsername, false)); + // 查询重复邮箱 + userImportResp + .setDuplicateEmailRows(countExistByField(validRowList, UserImportRowReq::getEmail, UserDO::getEmail, true)); + // 查询重复手机 + userImportResp + .setDuplicatePhoneRows(countExistByField(validRowList, UserImportRowReq::getPhone, UserDO::getPhone, true)); + + // 设置导入会话并缓存数据,有效期10分钟 + String importKey = UUID.fastUUID().toString(true); + RedisUtils.set(CacheConstants.DATA_IMPORT_KEY + importKey, JSONUtil.toJsonStr(validRowList), Duration + .ofMinutes(10)); + userImportResp.setImportKey(importKey); + return userImportResp; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public UserImportResp importUser(UserImportReq req) { + // 校验导入会话是否过期 + List importUserList; + try { + String data = RedisUtils.get(CacheConstants.DATA_IMPORT_KEY + req.getImportKey()); + importUserList = JSONUtil.toList(data, UserImportRowReq.class); + CheckUtils.throwIf(CollUtil.isEmpty(importUserList), "导入已过期,请重新上传"); + } catch (Exception e) { + log.error("导入异常:", e); + throw new BusinessException("导入已过期,请重新上传"); + } + // 已存在数据查询 + List existEmails = listExistByField(importUserList, UserImportRowReq::getEmail, UserDO::getEmail); + List existPhones = listExistByField(importUserList, UserImportRowReq::getPhone, UserDO::getPhone); + List existUserList = listByUsernames(importUserList.stream() + .map(UserImportRowReq::getUsername) + .filter(Objects::nonNull) + .toList()); + List existUsernames = existUserList.stream().map(UserDO::getUsername).toList(); + CheckUtils + .throwIf(isExitImportUser(req, importUserList, existUsernames, existEmails, existPhones), "数据不符合导入策略,已退出导入"); + + // 基础数据准备 + Map userMap = existUserList.stream() + .collect(Collectors.toMap(UserDO::getUsername, UserDO::getId)); + List roleList = roleService.listByNames(importUserList.stream() + .map(UserImportRowReq::getRoleName) + .distinct() + .toList()); + Map roleMap = roleList.stream().collect(Collectors.toMap(RoleDO::getName, RoleDO::getId)); + List deptList = deptService.listByNames(importUserList.stream() + .map(UserImportRowReq::getDeptName) + .distinct() + .toList()); + Map deptMap = deptList.stream().collect(Collectors.toMap(DeptDO::getName, DeptDO::getId)); + + // 批量操作数据库集合 + List insertList = new ArrayList<>(); + List updateList = new ArrayList<>(); + List userRoleDOList = new ArrayList<>(); + // ID生成器 + IdGenerator idGenerator = DefaultIdGeneratorProvider.INSTANCE.getShare(); + for (UserImportRowReq row : importUserList) { + if (isSkipUserImport(req, row, existUsernames, existPhones, existEmails)) { + // 按规则跳过该行 + continue; + } + UserDO userDO = BeanUtil.toBeanIgnoreError(row, UserDO.class); + userDO.setStatus(req.getDefaultStatus()); + userDO.setPwdResetTime(LocalDateTime.now()); + userDO.setGender(EnumUtil.getBy(GenderEnum::getDescription, row.getGender(), GenderEnum.UNKNOWN)); + // 修改 or 新增 + if (UPDATE.validate(req.getDuplicateUser(), row.getUsername(), existUsernames)) { + userDO.setId(userMap.get(row.getUsername())); + updateList.add(userDO); + } else { + userDO.setId(idGenerator.generate()); + userDO.setIsSystem(false); + insertList.add(userDO); + } + userRoleDOList.add(new UserRoleDO(userDO.getId(), roleMap.get(row.getRoleName()))); + } + doImportUser(insertList, updateList, userRoleDOList); + RedisUtils.delete(CacheConstants.DATA_IMPORT_KEY + req.getImportKey()); + return new UserImportResp(insertList.size() + updateList.size(), insertList.size(), updateList.size()); + } + + @Override + public void resetPassword(UserPasswordResetReq req, Long id) { + super.getById(id); + baseMapper.lambdaUpdate() + .set(UserDO::getPassword, req.getNewPassword()) + .set(UserDO::getPwdResetTime, LocalDateTime.now()) + .eq(UserDO::getId, id) + .update(); + } + + @Override + public void updateRole(UserRoleUpdateReq updateReq, Long id) { + super.getById(id); + List roleIds = updateReq.getRoleIds(); + // 保存用户和角色关联 + userRoleService.assignRolesToUser(roleIds, id); + // 更新用户上下文 + this.updateContext(id); + } + + @Override + public String updateAvatar(MultipartFile avatarFile, Long id) throws IOException { + String avatarImageType = FileNameUtil.extName(avatarFile.getOriginalFilename()); + CheckUtils.throwIf(!StrUtil.equalsAnyIgnoreCase(avatarImageType, avatarSupportSuffix), "头像仅支持 {} 格式的图片", String + .join(StringConstants.CHINESE_COMMA, avatarSupportSuffix)); + // 上传新头像 + UserDO user = super.getById(id); + FileInfo fileInfo = fileService.upload(avatarFile, avatarPath, null, false); + // 更新用户头像 + String newAvatar = fileInfo.getUrl(); + baseMapper.lambdaUpdate().set(UserDO::getAvatar, newAvatar).eq(UserDO::getId, id).update(); + // 删除原头像 + String oldAvatar = user.getAvatar(); + if (StrUtil.isNotBlank(oldAvatar)) { + fileStorageService.delete(oldAvatar); + } + return newAvatar; + } + + @Override + @CacheUpdate(key = "#id", value = "#req.nickname", name = CacheConstants.USER_KEY_PREFIX) + public void updateBasicInfo(UserBasicInfoUpdateReq req, Long id) { + super.getById(id); + baseMapper.lambdaUpdate() + .set(UserDO::getUsername, req.getUserName()) + .set(UserDO::getGender, req.getGender()) + .eq(UserDO::getId, id) + .update(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updatePassword(String oldPassword, String newPassword, Long id) { + CheckUtils.throwIfEqual(newPassword, oldPassword, "新密码不能与当前密码相同"); + UserDO user = super.getById(id); + String password = user.getPassword(); + if (StrUtil.isNotBlank(password)) { + CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, password), "当前密码错误"); + } + // 校验密码合法性 + int passwordRepetitionTimes = this.checkPassword(newPassword, user); + // 更新密码和密码重置时间 + baseMapper.lambdaUpdate() + .set(UserDO::getPassword, newPassword) + .set(UserDO::getPwdResetTime, LocalDateTime.now()) + .eq(UserDO::getId, id) + .update(); + // 保存历史密码 + userPasswordHistoryService.add(id, password, passwordRepetitionTimes); + // 修改后登出 + StpUtil.logout(); + } + + @Override + public void updatePhone(String newPhone, String oldPassword, Long id) { + UserDO user = super.getById(id); + CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, user.getPassword()), "当前密码错误"); + CheckUtils.throwIf(this.isPhoneExists(newPhone, id), "手机号已绑定其他账号,请更换其他手机号"); + CheckUtils.throwIfEqual(newPhone, user.getPhone(), "新手机号不能与当前手机号相同"); + // 更新手机号 + baseMapper.lambdaUpdate().set(UserDO::getPhone, newPhone).eq(UserDO::getId, id).update(); + } + + @Override + public void updateEmail(String newEmail, String oldPassword, Long id) { + UserDO user = super.getById(id); + CheckUtils.throwIf(!passwordEncoder.matches(oldPassword, user.getPassword()), "当前密码错误"); + CheckUtils.throwIf(this.isEmailExists(newEmail, id), "邮箱已绑定其他账号,请更换其他邮箱"); + CheckUtils.throwIfEqual(newEmail, user.getEmail(), "新邮箱不能与当前邮箱相同"); + // 更新邮箱 + baseMapper.lambdaUpdate().set(UserDO::getEmail, newEmail).eq(UserDO::getId, id).update(); + } + + @Override + public UserDO getByUsername(String username) { + return baseMapper.selectByUsername(username); + } + + @Override + public UserDO getByPhone(String phone) { + return baseMapper.selectByPhone(phone); + } + + @Override + public UserDO getByEmail(String email) { + return baseMapper.selectByEmail(email); + } + + @Override + public UserDO getByCard(String card) { + return baseMapper.selectByCard(card); + } + + @Override + public Long countByDeptIds(List deptIds) { + return 0L; + } + + @Override + protected List list(UserQuery query, SortQuery sortQuery, Class targetClass) { + QueryWrapper queryWrapper = this.buildQueryWrapper(query); + // 设置排序 + super.sort(queryWrapper, sortQuery); + List entityList = baseMapper.selectUserList(queryWrapper); + if (this.getEntityClass() == targetClass) { + return (List)entityList; + } + return BeanUtil.copyToList(entityList, targetClass); + } + + @Override + protected QueryWrapper buildQueryWrapper(UserQuery query) { + String description = query.getDescription(); + DisEnableStatusEnum status = query.getStatus(); + List createTimeList = query.getCreateTime(); + Long deptId = query.getDeptId(); + List userIdList = query.getUserIds(); + // 获取排除用户 ID 列表 + List excludeUserIdList = null; + if (null != query.getRoleId()) { + excludeUserIdList = userRoleService.listUserIdByRoleId(query.getRoleId()); + } + return new QueryWrapper().and(StrUtil.isNotBlank(description), q -> q.like("t1.username", description) + .or() + .like("t1.nickname", description) + .or() + .like("t1.description", description)) + .eq(null != status, "t1.status", status) + .between(CollUtil.isNotEmpty(createTimeList), "t1.create_time", CollUtil.getFirst(createTimeList), CollUtil + .getLast(createTimeList)) + .and(null != deptId && !SysConstants.SUPER_DEPT_ID.equals(deptId), q -> { + List deptIdList = deptService.listChildren(deptId) + .stream() + .map(DeptDO::getId) + .collect(Collectors.toList()); + deptIdList.add(deptId); + q.in("t1.dept_id", deptIdList); + }) + .in(CollUtil.isNotEmpty(userIdList), "t1.id", userIdList) + .notIn(CollUtil.isNotEmpty(excludeUserIdList), "t1.id", excludeUserIdList); + } + + /** + * 导入用户 + * + * @param insertList 新增用户 + * @param updateList 修改用户 + * @param userRoleDOList 用户角色关联 + */ + private void doImportUser(List insertList, List updateList, List userRoleDOList) { + if (CollUtil.isNotEmpty(insertList)) { + baseMapper.insert(insertList); + } + if (CollUtil.isNotEmpty(updateList)) { + SpringUtils.getProxy(this).updateBatchById(updateList); + userRoleService.deleteByUserIds(updateList.stream().map(UserDO::getId).toList()); + } + if (CollUtil.isNotEmpty(userRoleDOList)) { + userRoleService.saveBatch(userRoleDOList); + } + } + + /** + * 判断是否跳过导入 + * + * @param req 导入参数 + * @param row 导入数据 + * @param existUsernames 导入数据中已存在的用户名 + * @param existEmails 导入数据中已存在的邮箱 + * @param existPhones 导入数据中已存在的手机号 + * @return 是否跳过 + */ + private boolean isSkipUserImport(UserImportReq req, + UserImportRowReq row, + List existUsernames, + List existPhones, + List existEmails) { + return SKIP.validate(req.getDuplicateUser(), row.getUsername(), existUsernames) || SKIP.validate(req + .getDuplicateEmail(), row.getEmail(), existEmails) || SKIP.validate(req.getDuplicatePhone(), row + .getPhone(), existPhones); + } + + /** + * 判断是否退出导入 + * + * @param req 导入参数 + * @param list 导入数据 + * @param existUsernames 导入数据中已存在的用户名 + * @param existEmails 导入数据中已存在的邮箱 + * @param existPhones 导入数据中已存在的手机号 + * @return 是否退出 + */ + private boolean isExitImportUser(UserImportReq req, + List list, + List existUsernames, + List existEmails, + List existPhones) { + return list.stream() + .anyMatch(row -> EXIT.validate(req.getDuplicateUser(), row.getUsername(), existUsernames) || EXIT + .validate(req.getDuplicateEmail(), row.getEmail(), existEmails) || EXIT.validate(req + .getDuplicatePhone(), row.getPhone(), existPhones)); + } + + /** + * 按指定数据集获取数据库已存在的数量 + * + * @param userRowList 导入的数据源 + * @param rowField 导入数据的字段 + * @param dbField 对比数据库的字段 + * @return 存在的数量 + */ + private int countExistByField(List userRowList, + Function rowField, + SFunction dbField, + boolean fieldEncrypt) { + List fieldValues = userRowList.stream().map(rowField).filter(Objects::nonNull).toList(); + if (fieldValues.isEmpty()) { + return 0; + } + return (int)this.count(Wrappers.lambdaQuery() + .in(dbField, fieldEncrypt ? SecureUtils.encryptFieldByAes(fieldValues) : fieldValues)); + } + + /** + * 按指定数据集获取数据库已存在内容 + * + * @param userRowList 导入的数据源 + * @param rowField 导入数据的字段 + * @param dbField 对比数据库的字段 + * @return 存在的内容 + */ + private List listExistByField(List userRowList, + Function rowField, + SFunction dbField) { + List fieldValues = userRowList.stream().map(rowField).filter(Objects::nonNull).toList(); + if (fieldValues.isEmpty()) { + return Collections.emptyList(); + } + List userDOList = baseMapper.selectList(Wrappers.lambdaQuery() + .in(dbField, SecureUtils.encryptFieldByAes(fieldValues)) + .select(dbField)); + return userDOList.stream().map(dbField).filter(Objects::nonNull).toList(); + } + + /** + * 过滤无效的导入用户数据(批量导入不严格校验数据) + * + * @param importRowList 导入数据 + */ + private List filterImportData(List importRowList) { + // 校验过滤 + List list = importRowList.stream() + .filter(row -> ValidationUtil.validate(row).isEmpty()) + .toList(); + // 用户名去重 + return list.stream() + .collect(Collectors.toMap(UserImportRowReq::getUsername, user -> user, (existing, replacement) -> existing)) + .values() + .stream() + .toList(); + } + + /** + * 检测密码合法性 + * + * @param password 密码 + * @param user 用户信息 + * @return 密码允许重复使用次数 + */ + private int checkPassword(String password, UserDO user) { + Map passwordPolicy = optionService.getByCategory(OptionCategoryEnum.PASSWORD); + // 密码最小长度 + PASSWORD_MIN_LENGTH.validate(password, MapUtil.getInt(passwordPolicy, PASSWORD_MIN_LENGTH.name()), user); + // 密码是否必须包含特殊字符 + PASSWORD_REQUIRE_SYMBOLS.validate(password, MapUtil.getInt(passwordPolicy, PASSWORD_REQUIRE_SYMBOLS + .name()), user); + // 密码是否允许包含正反序账号名 + PASSWORD_ALLOW_CONTAIN_USERNAME.validate(password, MapUtil + .getInt(passwordPolicy, PASSWORD_ALLOW_CONTAIN_USERNAME.name()), user); + // 密码重复使用次数 + int passwordRepetitionTimes = MapUtil.getInt(passwordPolicy, PASSWORD_REPETITION_TIMES.name()); + PASSWORD_REPETITION_TIMES.validate(password, passwordRepetitionTimes, user); + return passwordRepetitionTimes; + } + + /** + * 名称是否存在 + * + * @param name 名称 + * @param id ID + * @return 是否存在 + */ + private boolean isNameExists(String name, Long id) { + return baseMapper.lambdaQuery().eq(UserDO::getUsername, name).ne(null != id, UserDO::getId, id).exists(); + } + + /** + * 邮箱是否存在 + * + * @param email 邮箱 + * @param id ID + * @return 是否存在 + */ + private boolean isEmailExists(String email, Long id) { + Long count = baseMapper.selectCountByEmail(email, id); + return null != count && count > 0; + } + + /** + * 手机号码是否存在 + * + * @param phone 手机号码 + * @param id ID + * @return 是否存在 + */ + private boolean isPhoneExists(String phone, Long id) { + Long count = baseMapper.selectCountByPhone(phone, id); + return null != count && count > 0; + } + + /** + * 根据用户名获取用户列表 + * + * @param usernames 用户名列表 + * @return 用户列表 + */ + private List listByUsernames(List usernames) { + return this.list(Wrappers.lambdaQuery() + .in(UserDO::getUsername, usernames) + .select(UserDO::getId, UserDO::getUsername)); + } + + /** + * 更新用户上下文信息 + * + * @param id ID + */ + private void updateContext(Long id) { + UserContext userContext = UserContextHolder.getContext(id); + if (null != userContext) { + userContext.setRoles(roleService.listByUserId(id)); + userContext.setPermissions(roleService.listPermissionByUserId(id)); + UserContextHolder.setContext(userContext); + } + } + + @Override + @ContainerMethod(namespace = ContainerConstants.USER_NAME_LIST, type = MappingType.ORDER_OF_KEYS) + public List listNameByIds(List ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List roleList = baseMapper.lambdaQuery().select(UserDO::getUsername).in(UserDO::getId, ids).list(); + return roleList.stream().map(UserDO::getUsername).toList(); + } + + @Override + @ContainerMethod(namespace = ContainerConstants.USER_NAME, type = MappingType.ORDER_OF_KEYS) + public String userNameByIds(Long id) { + if (null == id) { + return ""; + } + + UserDO userDO = baseMapper.lambdaQuery().eq(UserDO::getId, id).one(); + return null == userDO ? "" : userDO.getUsername(); + } + +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserSocialServiceImpl.java b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserSocialServiceImpl.java new file mode 100644 index 0000000..c804012 --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/service/impl/UserSocialServiceImpl.java @@ -0,0 +1,77 @@ +package top.wms.admin.system.service.impl; + +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import me.zhyd.oauth.model.AuthUser; +import org.springframework.stereotype.Service; +import top.wms.admin.system.enums.SocialSourceEnum; +import top.wms.admin.system.mapper.UserSocialMapper; +import top.wms.admin.system.model.entity.UserSocialDO; +import top.wms.admin.system.service.UserSocialService; +import top.continew.starter.core.validation.CheckUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 用户社会化关联业务实现 + * + * @author Charles7c + * @since 2023/10/11 22:10 + */ +@Service +@RequiredArgsConstructor +public class UserSocialServiceImpl implements UserSocialService { + + private final UserSocialMapper baseMapper; + + @Override + public UserSocialDO getBySourceAndOpenId(String source, String openId) { + return baseMapper.selectBySourceAndOpenId(source, openId); + } + + @Override + public void saveOrUpdate(UserSocialDO userSocial) { + if (null == userSocial.getCreateTime()) { + baseMapper.insert(userSocial); + } else { + baseMapper.lambdaUpdate() + .set(UserSocialDO::getMetaJson, userSocial.getMetaJson()) + .set(UserSocialDO::getLastLoginTime, userSocial.getLastLoginTime()) + .eq(UserSocialDO::getSource, userSocial.getSource()) + .eq(UserSocialDO::getOpenId, userSocial.getOpenId()) + .update(); + } + } + + @Override + public List listByUserId(Long userId) { + return baseMapper.lambdaQuery().eq(UserSocialDO::getUserId, userId).list(); + } + + @Override + public void bind(AuthUser authUser, Long userId) { + String source = authUser.getSource(); + String openId = authUser.getUuid(); + List userSocialList = this.listByUserId(userId); + Set boundSocialSet = userSocialList.stream().map(UserSocialDO::getSource).collect(Collectors.toSet()); + String description = SocialSourceEnum.valueOf(source).getDescription(); + CheckUtils.throwIf(boundSocialSet.contains(source), "您已经绑定过了 [{}] 平台,请先解绑", description); + UserSocialDO userSocial = this.getBySourceAndOpenId(source, openId); + CheckUtils.throwIfNotNull(userSocial, "[{}] 平台账号 [{}] 已被其他用户绑定", description, authUser.getUsername()); + userSocial = new UserSocialDO(); + userSocial.setUserId(userId); + userSocial.setSource(source); + userSocial.setOpenId(openId); + userSocial.setMetaJson(JSONUtil.toJsonStr(authUser)); + userSocial.setLastLoginTime(LocalDateTime.now()); + baseMapper.insert(userSocial); + } + + @Override + public void deleteBySourceAndUserId(String source, Long userId) { + baseMapper.lambdaUpdate().eq(UserSocialDO::getSource, source).eq(UserSocialDO::getUserId, userId).remove(); + } +} diff --git a/mes-module-system/src/main/java/top/mes/admin/system/validation/ValidationGroup.java b/mes-module-system/src/main/java/top/mes/admin/system/validation/ValidationGroup.java new file mode 100644 index 0000000..200221e --- /dev/null +++ b/mes-module-system/src/main/java/top/mes/admin/system/validation/ValidationGroup.java @@ -0,0 +1,29 @@ +package top.wms.admin.system.validation; + +import jakarta.validation.groups.Default; + +/** + * 分组校验 + * + * @author Charles7c + * @since 2024/7/3 22:01 + */ +public interface ValidationGroup extends Default { + + /** + * 分组校验-增删改查 + */ + interface Storage extends ValidationGroup { + /** + * 本地存储 + */ + interface Local extends Storage { + } + + /** + * 对象存储 + */ + interface OSS extends Storage { + } + } +} \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/DeptMapper.xml b/mes-module-system/src/main/resources/mapper/DeptMapper.xml new file mode 100644 index 0000000..7e14587 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/DeptMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/DictItemMapper.xml b/mes-module-system/src/main/resources/mapper/DictItemMapper.xml new file mode 100644 index 0000000..108771f --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/DictItemMapper.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/LogMapper.xml b/mes-module-system/src/main/resources/mapper/LogMapper.xml new file mode 100644 index 0000000..4b64a30 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/LogMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/mes-module-system/src/main/resources/mapper/MenuMapper.xml b/mes-module-system/src/main/resources/mapper/MenuMapper.xml new file mode 100644 index 0000000..e445e25 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/MenuMapper.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/RoleMapper.xml b/mes-module-system/src/main/resources/mapper/RoleMapper.xml new file mode 100644 index 0000000..23e1f43 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/RoleMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/RoleMenuMapper.xml b/mes-module-system/src/main/resources/mapper/RoleMenuMapper.xml new file mode 100644 index 0000000..30c501b --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/RoleMenuMapper.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/UserMapper.xml b/mes-module-system/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..54b90f0 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,57 @@ + + + + + + SELECT + t1.id, + t1.card_no, + t1.create_user, + t1.create_time, + t1.update_user, + t1.update_time, + t1.username, + t1.nickname, + t1.password, + t1.gender, + t1.email, + t1.phone, + t1.avatar, + t1.description, + t1.status, + t1.is_system, + t1.pwd_reset_time, + t1.dept_id, + t2.name AS deptName + FROM sys_user AS t1 + LEFT JOIN sys_dept AS t2 ON t2.id = t1.dept_id + + + + + + + + + + diff --git a/mes-module-system/src/main/resources/mapper/UserPasswordHistoryMapper.xml b/mes-module-system/src/main/resources/mapper/UserPasswordHistoryMapper.xml new file mode 100644 index 0000000..d82044c --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/UserPasswordHistoryMapper.xml @@ -0,0 +1,15 @@ + + + + + DELETE t1 FROM sys_user_password_history AS t1 + LEFT JOIN ( + SELECT id + FROM sys_user_password_history + WHERE user_id = #{userId} + ORDER BY create_time DESC + LIMIT #{count} + ) AS t2 ON t2.id = t1.id + WHERE t2.id IS NULL + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/UserRoleMapper.xml b/mes-module-system/src/main/resources/mapper/UserRoleMapper.xml new file mode 100644 index 0000000..f01b4e9 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/UserRoleMapper.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/mes-module-system/src/main/resources/mapper/UserSocialMapper.xml b/mes-module-system/src/main/resources/mapper/UserSocialMapper.xml new file mode 100644 index 0000000..4fee4e7 --- /dev/null +++ b/mes-module-system/src/main/resources/mapper/UserSocialMapper.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/pom.xml b/mes-plugin/mes-plugin-generator/pom.xml new file mode 100644 index 0000000..d0ac028 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + top.wms + wms-plugin + ${revision} + + + wms-plugin-generator + 代码生成器插件 + \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/config/properties/GeneratorProperties.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/config/properties/GeneratorProperties.java new file mode 100644 index 0000000..57319c3 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/config/properties/GeneratorProperties.java @@ -0,0 +1,75 @@ +package top.wms.admin.generator.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import top.continew.starter.data.core.enums.DatabaseType; + +import java.util.List; +import java.util.Map; + +/** + * 代码生成器配置属性 + * + * @author Charles7c + * @since 2023/8/5 11:08 + */ +@Data +@Component +@ConfigurationProperties(prefix = "generator") +public class GeneratorProperties { + + /** + * 排除数据表(哪些数据表不展示在代码生成中) + */ + private String[] excludeTables = new String[0]; + + /** + * 类型映射 + */ + private Map>> typeMappings = MapUtil.newHashMap(); + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 + */ + private String templatePath; + + /** + * 包名称 + */ + private String packageName; + + /** + * 排除字段 + */ + private String[] excludeFields; + + /** + * 扩展名 + */ + private String extension = FileNameUtil.EXT_JAVA; + + /** + * 后缀 + */ + private String suffix; + + /** + * 是否为后端模板 + */ + private boolean backend = true; + } +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/enums/FormTypeEnum.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/enums/FormTypeEnum.java new file mode 100644 index 0000000..a89e432 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/enums/FormTypeEnum.java @@ -0,0 +1,79 @@ +package top.wms.admin.generator.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 表单类型枚举 + * + * @author Charles7c + * @since 2023/8/6 10:49 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements BaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(2, "数字输入框"), + + /** + * 密码输入框 + */ + INPUT_PASSWORD(3, "密码输入框"), + + /** + * 文本域 + */ + TEXT_AREA(4, "文本域"), + + /** + * 下拉框 + */ + SELECT(5, "下拉框"), + + /** + * 单选框 + */ + RADIO(6, "单选框"), + + /** + * 开关 + */ + SWITCH(7, "开关"), + + /** + * 复选框 + */ + CHECK_BOX(8, "复选框"), + + /** + * 树形选择 + */ + TREE_SELECT(9, "树选择"), + + /** + * 时间框 + */ + TIME(10, "时间框"), + + /** + * 日期框 + */ + DATE(11, "日期框"), + + /** + * 时间框 + */ + DATE_TIME(12, "日期时间框"),; + + private final Integer value; + private final String description; +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/enums/QueryTypeEnum.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/enums/QueryTypeEnum.java new file mode 100644 index 0000000..658f4df --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/enums/QueryTypeEnum.java @@ -0,0 +1,89 @@ +package top.wms.admin.generator.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 查询类型枚举 + * + * @author Charles7c + * @since 2023/8/6 10:49 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements BaseEnum { + + /** + * 等于 =,例如:WHERE age = 18 + */ + EQ(1, "="), + + /** + * 不等于 !=,例如:WHERE age != 18 + */ + NE(2, "!="), + + /** + * 大于 >,例如:WHERE age > 18 + */ + GT(3, ">"), + + /** + * 大于等于 >= ,例如:WHERE age >= 18 + */ + GE(4, ">="), + + /** + * 小于 <,例如:WHERE age < 18 + */ + LT(5, "<"), + + /** + * 小于等于 <=,例如:WHERE age <= 18 + */ + LE(6, "<="), + + /** + * 范围查询,例如:WHERE age BETWEEN 10 AND 18 + */ + BETWEEN(7, "BETWEEN"), + + /** + * LIKE '%值%',例如:WHERE nickname LIKE '%s%' + */ + LIKE(8, "LIKE '%s%'"), + + /** + * LIKE '%值',例如:WHERE nickname LIKE '%s' + */ + LIKE_LEFT(9, "LIKE '%s'"), + + /** + * LIKE '值%',例如:WHERE nickname LIKE 's%' + */ + LIKE_RIGHT(10, "LIKE 's%'"), + + /** + * 包含查询,例如:WHERE age IN (10, 20, 30) + */ + IN(11, "IN"), + + /** + * 不包含查询,例如:WHERE age NOT IN (20, 30) + */ + NOT_IN(12, "NOT IN"), + + /** + * 空查询,例如:WHERE email IS NULL + */ + IS_NULL(13, "IS NULL"), + + /** + * 非空查询,例如:WHERE email IS NOT NULL + */ + IS_NOT_NULL(14, "IS NOT NULL"),; + + private final Integer value; + private final String description; +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/mapper/FieldConfigMapper.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/mapper/FieldConfigMapper.java new file mode 100644 index 0000000..6924109 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/mapper/FieldConfigMapper.java @@ -0,0 +1,26 @@ +package top.wms.admin.generator.mapper; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import top.wms.admin.generator.model.entity.FieldConfigDO; +import top.continew.starter.data.mp.base.BaseMapper; + +import java.util.List; + +/** + * 字段配置 Mapper + * + * @author Charles7c + * @since 2023/4/12 23:56 + */ +public interface FieldConfigMapper extends BaseMapper { + + /** + * 根据表名称查询 + * + * @param tableName 表名称 + * @return 字段配置信息 + */ + @Select("SELECT * FROM gen_field_config WHERE table_name = #{tableName} ORDER BY field_sort ASC") + List selectListByTableName(@Param("tableName") String tableName); +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/mapper/GenConfigMapper.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/mapper/GenConfigMapper.java new file mode 100644 index 0000000..11104d5 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/mapper/GenConfigMapper.java @@ -0,0 +1,13 @@ +package top.wms.admin.generator.mapper; + +import top.wms.admin.generator.model.entity.GenConfigDO; +import top.continew.starter.data.mp.base.BaseMapper; + +/** + * 生成配置 Mapper + * + * @author Charles7c + * @since 2023/4/12 23:56 + */ +public interface GenConfigMapper extends BaseMapper { +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/FieldConfigDO.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/FieldConfigDO.java new file mode 100644 index 0000000..42ab968 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/FieldConfigDO.java @@ -0,0 +1,172 @@ +package top.wms.admin.generator.model.entity; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.meta.Column; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import top.wms.admin.generator.enums.FormTypeEnum; +import top.wms.admin.generator.enums.QueryTypeEnum; +import top.continew.starter.core.constant.StringConstants; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 字段配置实体 + * + * @author Charles7c + * @since 2023/4/12 20:21 + */ +@Data +@NoArgsConstructor +@TableName("gen_field_config") +@Schema(description = "字段配置信息") +public class FieldConfigDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + private Long id; + + /** + * 表名称 + */ + @Schema(description = "表名称", example = "sys_user") + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** + * 列名称 + */ + @Schema(description = "列名称", example = "nickname") + @NotBlank(message = "列名称不能为空") + private String columnName; + + /** + * 列类型 + */ + @Schema(description = "列类型", example = "varchar") + @NotBlank(message = "列类型不能为空") + private String columnType; + + /** + * 列大小 + */ + @Schema(description = "列大小", example = "255") + private Long columnSize; + + /** + * 字段名称 + */ + @Schema(description = "字段名称", example = "nickname") + @NotBlank(message = "字段名称不能为空") + private String fieldName; + + /** + * 字段类型 + */ + @Schema(description = "字段类型", example = "String") + @NotBlank(message = "字段类型不能为空") + private String fieldType; + + /** + * 字段排序 + */ + @Schema(description = "字段排序", example = "字段排序") + @NotNull(message = "字段排序不能为空") + private Integer fieldSort; + + /** + * 注释 + */ + @Schema(description = "注释", example = "昵称") + private String comment; + + /** + * 是否必填 + */ + @Schema(description = "是否必填", example = "true") + private Boolean isRequired; + + /** + * 是否在列表中显示 + */ + @Schema(description = "是否在列表中显示", example = "true") + private Boolean showInList; + + /** + * 是否在表单中显示 + */ + @Schema(description = "是否在表单中显示", example = "true") + private Boolean showInForm; + + /** + * 是否在查询中显示 + */ + @Schema(description = "是否在查询中显示", example = "true") + private Boolean showInQuery; + + /** + * 表单类型 + */ + @Schema(description = "表单类型", example = "1") + private FormTypeEnum formType; + + /** + * 查询方式 + */ + @Schema(description = "查询方式", example = "1") + private QueryTypeEnum queryType; + + /** + * 字典编码 + */ + @Schema(description = "字典编码", example = "notice_type") + private String dictCode; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + public FieldConfigDO(@NonNull Column column) { + this.setTableName(column.getTableName()); + this.setColumnName(column.getName()); + this.setColumnType(column.getTypeName()); + this.setColumnSize(column.getSize()); + this.setComment(column.getComment()); + this.setIsRequired(!column.isPk() && !column.isNullable()); + this.setShowInList(true); + this.setShowInForm(this.getIsRequired()); + this.setShowInQuery(this.getIsRequired()); + } + + public void setColumnName(String columnName) { + this.columnName = columnName; + this.fieldName = StrUtil.toCamelCase(this.columnName); + } + + public void setColumnType(String columnType) { + String[] arr = StrUtil.splitToArray(columnType, StringConstants.SPACE); + this.columnType = arr.length > 1 ? arr[0].toLowerCase() : columnType.toLowerCase(); + } + + public void setComment(String comment) { + this.comment = StrUtil.nullToDefault(comment, StringConstants.EMPTY); + } +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/GenConfigDO.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/GenConfigDO.java new file mode 100644 index 0000000..4d943c7 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/GenConfigDO.java @@ -0,0 +1,132 @@ +package top.wms.admin.generator.model.entity; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.common.constant.RegexConstants; +import top.continew.starter.core.constant.CharConstants; +import top.continew.starter.core.util.StrUtils; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 生成配置实体 + * + * @author Charles7c + * @since 2023/4/12 20:21 + */ +@Data +@NoArgsConstructor +@TableName("gen_config") +@Schema(description = "生成配置信息") +public class GenConfigDO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 表名称 + */ + @Schema(description = "表名称", example = "sys_user") + @TableId(type = IdType.INPUT) + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** + * 描述 + */ + @Schema(description = "描述", example = "用户表") + @TableField(exist = false) + private String comment; + + /** + * 模块名称 + */ + @Schema(description = "模块名称", example = "wms-admin-system") + @NotBlank(message = "模块名称不能为空") + @Length(max = 60, message = "模块名称不能超过 {max} 个字符") + private String moduleName; + + /** + * 包名称 + */ + @Schema(description = "包名称", example = "top.wms.admin.system") + @NotBlank(message = "包名称不能为空") + @Pattern(regexp = RegexConstants.PACKAGE_NAME, message = "包名称格式错误") + @Length(max = 60, message = "包名称不能超过 {max} 个字符") + private String packageName; + + /** + * 业务名称 + */ + @Schema(description = "业务名称", example = "用户") + @NotBlank(message = "业务名称不能为空") + @Length(max = 50, message = "业务名称不能超过 {max} 个字符") + private String businessName; + + /** + * 作者 + */ + @Schema(description = "作者", example = "Charles7c") + @NotBlank(message = "作者名称不能为空") + @Length(max = 100, message = "作者名称不能超过 {max} 个字符") + private String author; + + /** + * 表前缀 + */ + @Schema(description = "表前缀", example = "sys_") + private String tablePrefix; + + /** + * 是否覆盖 + */ + @Schema(description = "是否覆盖", example = "false") + @NotNull(message = "是否覆盖不能为空") + private Boolean isOverride; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @Schema(description = "修改时间", example = "2023-08-08 08:08:08", type = "string") + @TableField(fill = FieldFill.UPDATE) + private LocalDateTime updateTime; + + public GenConfigDO(String tableName) { + this.setTableName(tableName); + } + + public void setTableName(String tableName) { + this.tableName = tableName; + // 默认表前缀(sys_user -> sys_) + int underLineIndex = StrUtil.indexOf(tableName, CharConstants.UNDERLINE); + if (-1 != underLineIndex) { + this.tablePrefix = StrUtil.subPre(tableName, underLineIndex + 1); + } + } + + /** + * 类名前缀 + */ + @Schema(description = "类名前缀", example = "User") + public String getClassNamePrefix() { + String rawClassName = StrUtils.blankToDefault(this.getTablePrefix(), this.getTableName(), prefix -> StrUtil + .removePrefix(this.getTableName(), prefix)); + return StrUtil.upperFirst(StrUtil.toCamelCase(rawClassName)); + } +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/InnerGenConfigDO.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/InnerGenConfigDO.java new file mode 100644 index 0000000..87bd154 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/entity/InnerGenConfigDO.java @@ -0,0 +1,108 @@ +package top.wms.admin.generator.model.entity; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.EqualsAndHashCode; +import top.continew.starter.core.constant.StringConstants; + +import java.io.Serial; +import java.util.List; +import java.util.Set; + +/** + * 内部生成配置信息 + * + * @author Charles7c + * @since 2024/8/30 19:35 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class InnerGenConfigDO extends GenConfigDO { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 字段配置信息 + */ + private List fieldConfigs; + + /** + * 生成时间 + */ + private String datetime; + + /** + * API 模块名称 + */ + private String apiModuleName; + + /** + * API 名称 + */ + private String apiName; + + /** + * 类名 + */ + private String className; + + /** + * 类名前缀 + */ + private String classNamePrefix; + + /** + * 子包名称 + */ + private String subPackageName; + + /** + * 字典编码列表 + */ + private Set dictCodes; + + /** + * 是否包含必填字段 + */ + private boolean hasRequiredField; + + /** + * 是否包含字典字段 + */ + private boolean hasDictField; + + /** + * 是否包含 BigDecimal 字段 + */ + private boolean hasBigDecimalField; + + /** + * 是否包含 Time 包字段 + */ + private boolean hasTimeField; + + public InnerGenConfigDO() { + } + + public InnerGenConfigDO(GenConfigDO genConfig) { + BeanUtil.copyProperties(genConfig, this); + this.setDatetime(DateUtil.date().toString("yyyy/MM/dd HH:mm")); + this.setApiName(StrUtil.lowerFirst(this.getClassNamePrefix())); + } + + @Override + public void setPackageName(String packageName) { + super.setPackageName(packageName); + String realPackageName = this.getPackageName(); + this.setApiModuleName(StrUtil.subSuf(realPackageName, StrUtil + .lastIndexOfIgnoreCase(realPackageName, StringConstants.DOT) + 1)); + } + + @Override + public String getClassNamePrefix() { + return super.getClassNamePrefix(); + } +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/query/GenConfigQuery.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/query/GenConfigQuery.java new file mode 100644 index 0000000..7608327 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/query/GenConfigQuery.java @@ -0,0 +1,27 @@ +package top.wms.admin.generator.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 生成配置查询条件 + * + * @author Charles7c + * @since 2023/4/12 20:21 + */ +@Data +@Schema(description = "生成配置查询条件") +public class GenConfigQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 表名称 + */ + @Schema(description = "表名称", example = "sys_user") + private String tableName; +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/req/GenConfigReq.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/req/GenConfigReq.java new file mode 100644 index 0000000..9c3f7a2 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/req/GenConfigReq.java @@ -0,0 +1,44 @@ +package top.wms.admin.generator.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import top.wms.admin.generator.model.entity.FieldConfigDO; +import top.wms.admin.generator.model.entity.GenConfigDO; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 代码生成配置信息 + * + * @author Charles7c + * @since 2023/8/8 20:40 + */ +@Data +@Schema(description = "代码生成配置信息") +public class GenConfigReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 字段配置信息 + */ + @Valid + @Schema(description = "字段配置信息") + @NotEmpty(message = "字段配置不能为空") + private List fieldConfigs = new ArrayList<>(); + + /** + * 生成配置信息 + */ + @Valid + @Schema(description = "生成配置信息") + @NotNull(message = "生成配置不能为空") + private GenConfigDO genConfig; +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/resp/GeneratePreviewResp.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/resp/GeneratePreviewResp.java new file mode 100644 index 0000000..2ddaca8 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/model/resp/GeneratePreviewResp.java @@ -0,0 +1,44 @@ +package top.wms.admin.generator.model.resp; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 生成预览信息 + * + * @author Charles7c + * @since 2023/12/19 21:34 + */ +@Data +@Schema(description = "生成预览信息") +public class GeneratePreviewResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "生成的文件路径", example = "wms-admin\\wms-admin\\wms-admin-generator\\src\\main\\java\\top\\wms\\admin\\generator\\service") + private String path; + + /** + * 文件名 + */ + @Schema(description = "文件名", example = "UserController.java") + private String fileName; + + /** + * 内容 + */ + @Schema(description = "内容", example = "public class UserController {...}") + private String content; + + /** + * 是否为后端代码 + */ + @Schema(hidden = true) + @JsonIgnore + private boolean backend; +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/service/GeneratorService.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/service/GeneratorService.java new file mode 100644 index 0000000..77f078e --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/service/GeneratorService.java @@ -0,0 +1,80 @@ +package top.wms.admin.generator.service; + +import jakarta.servlet.http.HttpServletResponse; +import top.wms.admin.generator.model.entity.FieldConfigDO; +import top.wms.admin.generator.model.entity.GenConfigDO; +import top.wms.admin.generator.model.query.GenConfigQuery; +import top.wms.admin.generator.model.req.GenConfigReq; +import top.wms.admin.generator.model.resp.GeneratePreviewResp; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.sql.SQLException; +import java.util.List; + +/** + * 代码生成业务接口 + * + * @author Charles7c + * @since 2023/4/12 23:57 + */ +public interface GeneratorService { + + /** + * 分页查询生成配置列表 + * + * @param query 查询条件 + * @param pageQuery 分页查询条件 + * @return 分页列表信息 + */ + PageResp pageGenConfig(GenConfigQuery query, PageQuery pageQuery); + + /** + * 查询生成配置信息 + * + * @param tableName 表名称 + * @return 生成配置信息 + * @throws SQLException / + */ + GenConfigDO getGenConfig(String tableName) throws SQLException; + + /** + * 查询字段配置列表 + * + * @param tableName 表名称 + * @param requireSync 是否需要同步 + * @return 字段配置列表 + */ + List listFieldConfig(String tableName, Boolean requireSync); + + /** + * 保存代码生成配置信息 + * + * @param req 代码生成配置信息 + * @param tableName 表名称 + */ + void saveConfig(GenConfigReq req, String tableName); + + /** + * 生成预览 + * + * @param tableNames 表名称列表 + * @return 预览信息 + */ + List preview(List tableNames); + + /** + * 生成下载代码 + * + * @param tableNames 表名称列表 + * @param response 响应对象 + */ + void downloadCode(List tableNames, HttpServletResponse response); + + /** + * 生成下载代码 + * + * @param tableNames 表名称列表 + */ + void generateCode(List tableNames); +} diff --git a/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/service/impl/GeneratorServiceImpl.java b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/service/impl/GeneratorServiceImpl.java new file mode 100644 index 0000000..7566830 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/java/top/mes/admin/generator/service/impl/GeneratorServiceImpl.java @@ -0,0 +1,433 @@ +package top.wms.admin.generator.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; +import cn.hutool.db.meta.Column; +import cn.hutool.db.meta.Table; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine; +import cn.hutool.system.SystemUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.DefaultObjectWrapperBuilder; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.generator.config.properties.GeneratorProperties; +import top.wms.admin.generator.enums.FormTypeEnum; +import top.wms.admin.generator.enums.QueryTypeEnum; +import top.wms.admin.generator.mapper.FieldConfigMapper; +import top.wms.admin.generator.mapper.GenConfigMapper; +import top.wms.admin.generator.model.entity.FieldConfigDO; +import top.wms.admin.generator.model.entity.GenConfigDO; +import top.wms.admin.generator.model.entity.InnerGenConfigDO; +import top.wms.admin.generator.model.query.GenConfigQuery; +import top.wms.admin.generator.model.req.GenConfigReq; +import top.wms.admin.generator.model.resp.GeneratePreviewResp; +import top.wms.admin.generator.service.GeneratorService; +import top.continew.starter.core.autoconfigure.project.ProjectProperties; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.data.core.enums.DatabaseType; +import top.continew.starter.data.core.util.MetaUtils; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; +import top.continew.starter.web.util.FileUploadUtils; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.SQLException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 代码生成业务实现 + * + * @author Charles7c + * @since 2023/4/12 23:58 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class GeneratorServiceImpl implements GeneratorService { + + private final DataSource dataSource; + private final GeneratorProperties generatorProperties; + private final ProjectProperties projectProperties; + private final FieldConfigMapper fieldConfigMapper; + private final GenConfigMapper genConfigMapper; + private static final List TIME_PACKAGE_CLASS = Arrays.asList("LocalDate", "LocalTime", "LocalDateTime"); + + @Override + public PageResp pageGenConfig(GenConfigQuery query, PageQuery pageQuery) { + // 查询所有表 + List tableList = MetaUtils.getTables(dataSource); + tableList.removeIf(table -> StrUtil.equalsAnyIgnoreCase(table.getTableName(), generatorProperties + .getExcludeTables())); + String tableName = query.getTableName(); + if (StrUtil.isNotBlank(tableName)) { + tableList.removeIf(table -> !StrUtil.containsAnyIgnoreCase(table.getTableName(), tableName)); + } + // 查询生成配置 + List list = tableList.parallelStream().map(table -> { + GenConfigDO genConfig = genConfigMapper.selectById(table.getTableName()); + if (genConfig == null) { + genConfig = new GenConfigDO(table.getTableName()); + } + genConfig.setComment(table.getComment()); + return genConfig; + }) + .sorted(Comparator.comparing(GenConfigDO::getTableName) + .thenComparing(GenConfigDO::getUpdateTime, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(GenConfigDO::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder()))) + .toList(); + // 分页 + return PageResp.build(pageQuery.getPage(), pageQuery.getSize(), list); + } + + @Override + public GenConfigDO getGenConfig(String tableName) throws SQLException { + GenConfigDO genConfig = genConfigMapper.selectById(tableName); + if (null == genConfig) { + genConfig = new GenConfigDO(tableName); + // 默认包名(当前包名) + String packageName = ClassUtil.getPackage(GeneratorService.class); + genConfig.setPackageName(StrUtil.subBefore("top.wms.admin.", StringConstants.DOT, true)); + genConfig.setModuleName("wms-module-system"); + // 默认业务名(表注释) + List
tableList = MetaUtils.getTables(dataSource, tableName); + if (CollUtil.isNotEmpty(tableList)) { + Table table = tableList.get(0); + genConfig.setBusinessName(StrUtil.replace(table.getComment(), "表", StringConstants.EMPTY)); + } + // 默认作者名称(上次保存使用的作者名称) + GenConfigDO lastGenConfig = genConfigMapper.selectOne(Wrappers.lambdaQuery(GenConfigDO.class) + .orderByDesc(GenConfigDO::getCreateTime) + .last("LIMIT 1")); + if (null != lastGenConfig) { + genConfig.setAuthor(lastGenConfig.getAuthor()); + } + } + return genConfig; + } + + @Override + public List listFieldConfig(String tableName, Boolean requireSync) { + List fieldConfigList = fieldConfigMapper.selectListByTableName(tableName); + if (CollUtil.isNotEmpty(fieldConfigList) && Boolean.FALSE.equals(requireSync)) { + return fieldConfigList; + } + List latestFieldConfigList = new ArrayList<>(); + // 获取最新数据表列信息 + Collection columnList = MetaUtils.getColumns(dataSource, tableName); + // 获取数据库对应的类型映射配置 + DatabaseType databaseType = MetaUtils.getDatabaseType(dataSource); + Map> typeMappingMap = generatorProperties.getTypeMappings().get(databaseType); + CheckUtils.throwIfEmpty(typeMappingMap, "请先配置对应数据库的类型映射"); + Set>> typeMappingEntrySet = typeMappingMap.entrySet(); + // 新增或更新字段配置 + Map fieldConfigMap = fieldConfigList.stream() + .collect(Collectors.toMap(FieldConfigDO::getColumnName, Function.identity(), (key1, key2) -> key2)); + int i = 1; + for (Column column : columnList) { + FieldConfigDO fieldConfig = Optional.ofNullable(fieldConfigMap.get(column.getName())) + .orElseGet(() -> new FieldConfigDO(column)); + // 更新已有字段配置 + if (null != fieldConfig.getCreateTime()) { + fieldConfig.setColumnType(column.getTypeName()); + fieldConfig.setColumnSize(column.getSize()); + } + String fieldType = typeMappingEntrySet.stream() + .filter(entry -> entry.getValue().contains(fieldConfig.getColumnType())) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + fieldConfig.setFieldType(fieldType); + fieldConfig.setFieldSort(i++); + latestFieldConfigList.add(fieldConfig); + } + return latestFieldConfigList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveConfig(GenConfigReq req, String tableName) { + // 保存字段配置(先删除再保存) + fieldConfigMapper.delete(Wrappers.lambdaQuery(FieldConfigDO.class).eq(FieldConfigDO::getTableName, tableName)); + List fieldConfigList = req.getFieldConfigs(); + for (int i = 0; i < fieldConfigList.size(); i++) { + FieldConfigDO fieldConfig = fieldConfigList.get(i); + // 重新设置排序 + fieldConfig.setFieldSort(i + 1); + if (Boolean.TRUE.equals(fieldConfig.getShowInForm())) { + fieldConfig.setFormType(ObjectUtil.defaultIfNull(fieldConfig.getFormType(), FormTypeEnum.INPUT)); + } else { + // 在表单中不显示,不需要设置必填 + fieldConfig.setIsRequired(false); + } + if (Boolean.TRUE.equals(fieldConfig.getShowInQuery())) { + fieldConfig.setFormType(ObjectUtil.defaultIfNull(fieldConfig.getFormType(), FormTypeEnum.INPUT)); + fieldConfig.setQueryType(ObjectUtil.defaultIfNull(fieldConfig.getQueryType(), QueryTypeEnum.EQ)); + } else { + // 在查询中不显示,不需要设置查询方式 + fieldConfig.setQueryType(null); + } + // 既不在表单也不在查询中显示,不需要设置表单类型 + if (Boolean.FALSE.equals(fieldConfig.getShowInForm()) && Boolean.FALSE.equals(fieldConfig + .getShowInQuery())) { + fieldConfig.setFormType(null); + } + fieldConfig.setTableName(tableName); + } + fieldConfigMapper.insert(fieldConfigList); + // 保存或更新生成配置信息 + GenConfigDO newGenConfig = req.getGenConfig(); + GenConfigDO oldGenConfig = genConfigMapper.selectById(tableName); + if (null != oldGenConfig) { + BeanUtil.copyProperties(newGenConfig, oldGenConfig); + genConfigMapper.updateById(oldGenConfig); + } else { + genConfigMapper.insert(newGenConfig); + } + } + + @Override + public List preview(List tableNames) { + List generatePreviewList = new ArrayList<>(); + for (String tableName : tableNames) { + generatePreviewList.addAll(this.preview(tableName)); + } + return generatePreviewList; + } + + @Override + public void downloadCode(List tableNames, HttpServletResponse response) { + try { + String tempDir = SystemUtil.getUserInfo().getTempDir(); + // 删除旧代码 + FileUtil.del(tempDir + projectProperties.getAppName()); + tableNames.forEach(tableName -> { + // 初始化配置及数据 + List generatePreviewList = this.preview(tableName); + // 生成代码 + this.generateCode(generatePreviewList, genConfigMapper.selectById(tableName)); + }); + // 打包下载 + File tempDirFile = new File(tempDir, projectProperties.getAppName()); + String zipFilePath = tempDirFile.getPath() + jodd.io.ZipUtil.ZIP_EXT; + ZipUtil.zip(tempDirFile.getPath(), zipFilePath); + FileUploadUtils.download(response, new File(zipFilePath)); + } catch (Exception e) { + log.error("Generate code of table '{}' occurred an error. {}", tableNames, e.getMessage(), e); + throw new BusinessException("代码生成失败,请手动清理生成文件"); + } + } + + @Override + public void generateCode(List tableNames) { + try { + String projectPath = System.getProperty("user.dir"); + tableNames.forEach(tableName -> { + // 初始化配置及数据 + List generatePreviewList = this.preview(tableName); + // 生成代码 + for (GeneratePreviewResp generatePreview : generatePreviewList) { + // 后端:wms-admin/wms-system/src/main/java/top/wms/admin/system/service/impl/XxxServiceImpl.java + // 前端:wms-admin/wms-admin-ui/src/views/system/user/index.vue + File file = new File(projectPath + generatePreview.getPath() + .replace("wms-admin\\wms-admin", ""), generatePreview.getFileName()); + // 如果已经存在,且不允许覆盖,则跳过 + if (!file.exists() || Boolean.TRUE.equals(genConfigMapper.selectById(tableName).getIsOverride())) { + FileUtil.writeUtf8String(generatePreview.getContent(), file); + } + } + }); + + } catch (Exception e) { + log.error("Generate code of table '{}' occurred an error. {}", tableNames, e.getMessage(), e); + throw new BusinessException("代码生成失败,请手动清理生成文件"); + } + } + + /** + * 生成预览 + * + * @param tableName 表名称 + * @return 预览信息 + */ + private List preview(String tableName) { + List generatePreviewList = new ArrayList<>(); + // 初始化配置 + GenConfigDO genConfig = genConfigMapper.selectById(tableName); + CheckUtils.throwIfNull(genConfig, "请先进行数据表 [{}] 生成配置", tableName); + List fieldConfigList = fieldConfigMapper.selectListByTableName(tableName); + CheckUtils.throwIfEmpty(fieldConfigList, "请先进行数据表 [{}] 字段配置", tableName); + InnerGenConfigDO innerGenConfig = new InnerGenConfigDO(genConfig); + // 渲染代码 + String classNamePrefix = innerGenConfig.getClassNamePrefix(); + Map templateConfigMap = generatorProperties.getTemplateConfigs(); + TemplateEngine engine = TemplateUtil + .createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + // 在模板中允许使用静态方法 + if (engine instanceof FreemarkerEngine freemarkerEngine) { + DefaultObjectWrapper wrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_33).build(); + freemarkerEngine.getConfiguration().setSharedVariable("statics", wrapper.getStaticModels()); + } + for (Map.Entry templateConfigEntry : templateConfigMap.entrySet()) { + GeneratorProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + // 移除需要忽略的字段 + innerGenConfig.setFieldConfigs(fieldConfigList.stream() + .filter(fieldConfig -> !StrUtil.equalsAny(fieldConfig.getFieldName(), templateConfig + .getExcludeFields())) + .toList()); + // 预处理配置 + this.pretreatment(innerGenConfig); + // 处理其他配置 + innerGenConfig.setSubPackageName(templateConfig.getPackageName()); + String classNameSuffix = templateConfigEntry.getKey(); + String className = classNamePrefix + StrUtil.blankToDefault(templateConfig.getSuffix(), classNameSuffix); + innerGenConfig.setClassName(className); + boolean isBackend = templateConfig.isBackend(); + String extension = templateConfig.getExtension(); + GeneratePreviewResp generatePreview = new GeneratePreviewResp(); + generatePreview.setBackend(isBackend); + generatePreviewList.add(generatePreview); + String fileName = className + extension; + if (!isBackend) { + fileName = ".vue".equals(extension) && "index".equals(classNameSuffix) + ? "index.vue" + : this.getFrontendFileName(classNamePrefix, className, extension); + } + generatePreview.setFileName(fileName); + generatePreview.setContent(engine.getTemplate(templateConfig.getTemplatePath()) + .render(BeanUtil.beanToMap(innerGenConfig))); + this.setPreviewPath(generatePreview, innerGenConfig, templateConfig); + } + return generatePreviewList; + } + + /** + * 设置预览路径 + * + * @param generatePreview 预览信息 + * @param genConfig 生成配置 + * @param templateConfig 模板配置 + */ + private void setPreviewPath(GeneratePreviewResp generatePreview, + InnerGenConfigDO genConfig, + GeneratorProperties.TemplateConfig templateConfig) { + // 获取前后端基础路径 + String backendBasicPackagePath = this.buildBackendBasicPackagePath(genConfig, templateConfig); + String frontendBasicPackagePath = String.join(File.separator, projectProperties.getAppName(), projectProperties + .getAppName() + "-ui"); + String packagePath; + if (generatePreview.isBackend()) { + // 例如:wms-admin/wms-system/src/main/java/top/wms/admin/system/service/impl + packagePath = String.join(File.separator, backendBasicPackagePath, templateConfig.getPackageName() + .replace(StringConstants.DOT, File.separator)); + } else { + // 例如:wms-admin/wms-admin-ui/src/views/system + packagePath = String.join(File.separator, frontendBasicPackagePath, templateConfig.getPackageName() + .replace(StringConstants.SLASH, File.separator), genConfig.getApiModuleName()); + // 例如:wms-admin/wms-admin-ui/src/views/system/user + packagePath = ".vue".equals(templateConfig.getExtension()) + ? packagePath + File.separator + StrUtil.lowerFirst(genConfig.getClassNamePrefix()) + : packagePath; + } + generatePreview.setPath(packagePath); + } + + /** + * 生成代码 + * + * @param generatePreviewList 生成预览列表 + * @param genConfig 生成配置 + */ + private void generateCode(List generatePreviewList, GenConfigDO genConfig) { + for (GeneratePreviewResp generatePreview : generatePreviewList) { + // 后端:wms-admin/wms-system/src/main/java/top/wms/admin/system/service/impl/XxxServiceImpl.java + // 前端:wms-admin/wms-admin-ui/src/views/system/user/index.vue + File file = new File(SystemUtil.getUserInfo().getTempDir() + generatePreview.getPath(), generatePreview + .getFileName()); + // 如果已经存在,且不允许覆盖,则跳过 + if (!file.exists() || Boolean.TRUE.equals(genConfig.getIsOverride())) { + FileUtil.writeUtf8String(generatePreview.getContent(), file); + } + } + } + + /** + * 构建后端包路径 + * + * @param genConfig 生成配置 + * @param templateConfig 模板配置 + * @return 后端包路径 + */ + private String buildBackendBasicPackagePath(GenConfigDO genConfig, + GeneratorProperties.TemplateConfig templateConfig) { + String extension = templateConfig.getExtension(); + // 例如:wms-admin/wms-system/src/main/java/top/wms/admin/system + return String.join(File.separator, projectProperties.getAppName(), projectProperties.getAppName(), genConfig + .getModuleName(), "src", "main", FileNameUtil.EXT_JAVA.equals(extension) + ? "java" + : "resources") + (FileNameUtil.EXT_JAVA.equals(extension) + ? File.separator + genConfig.getPackageName().replace(StringConstants.DOT, File.separator) + : StringConstants.EMPTY); + } + + /** + * 获取前端文件名 + * + * @param classNamePrefix 类名前缀 + * @param className 类名 + * @param extension 扩展名 + * @return 前端文件名 + */ + private String getFrontendFileName(String classNamePrefix, String className, String extension) { + return (".ts".equals(extension) ? StrUtil.lowerFirst(classNamePrefix) : className) + extension; + } + + /** + * 预处理生成配置 + * + * @param genConfig 生成配置 + */ + private void pretreatment(InnerGenConfigDO genConfig) { + List fieldConfigList = genConfig.getFieldConfigs(); + // 统计部分特殊字段特征 + Set dictCodeSet = new HashSet<>(); + for (FieldConfigDO fieldConfig : fieldConfigList) { + String fieldType = fieldConfig.getFieldType(); + // 必填项 + if (Boolean.TRUE.equals(fieldConfig.getIsRequired())) { + genConfig.setHasRequiredField(true); + } + // 数据类型 + if ("BigDecimal".equals(fieldType)) { + genConfig.setHasBigDecimalField(true); + } + if (TIME_PACKAGE_CLASS.contains(fieldType)) { + genConfig.setHasTimeField(true); + } + // 字典码 + if (StrUtil.isNotBlank(fieldConfig.getDictCode())) { + genConfig.setHasDictField(true); + dictCodeSet.add(fieldConfig.getDictCode()); + } + } + genConfig.setDictCodes(dictCodeSet); + } +} diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Controller.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Controller.ftl new file mode 100644 index 0000000..5f4778b --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Controller.ftl @@ -0,0 +1,30 @@ +package top.wms.admin.controller.${subPackageName}; + +import top.continew.starter.extension.crud.enums.Api; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.springframework.web.bind.annotation.*; + +import lombok.RequiredArgsConstructor; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.wms.admin.common.controller.BaseController; +import ${packageName}.model.query.${classNamePrefix}Query; +import ${packageName}.model.req.${classNamePrefix}Req; +import ${packageName}.model.resp.${classNamePrefix}DetailResp; +import ${packageName}.model.resp.${classNamePrefix}Resp; +import ${packageName}.service.${classNamePrefix}Service; + +/** + * ${businessName}管理 API + * + * @author ${author} + * @since ${datetime} + */ +@Tag(name = "${businessName}管理 API") +@RestController +@RequiredArgsConstructor +@CrudRequestMapping(value = "/${apiModuleName}/${apiName}", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) +public class ${className} extends BaseController<${classNamePrefix}Service, ${classNamePrefix}Resp, ${classNamePrefix}Resp, ${classNamePrefix}Query, ${classNamePrefix}Req> { + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/DetailResp.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/DetailResp.ftl new file mode 100644 index 0000000..d0646c9 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/DetailResp.ftl @@ -0,0 +1,44 @@ +package ${packageName}.${subPackageName}; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; + +import top.wms.admin.common.model.resp.BaseDetailResp; + +import java.io.Serial; +<#if hasTimeField> +import java.time.*; + +<#if hasBigDecimalField> +import java.math.BigDecimal; + + +/** + * ${businessName}详情信息 + * + * @author ${author} + * @since ${datetime} + */ +@Data +@ExcelIgnoreUnannotated +@Schema(description = "${businessName}详情信息") +public class ${className} extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; +<#if fieldConfigs??> + <#list fieldConfigs as fieldConfig> + + /** + * ${fieldConfig.comment} + */ + @Schema(description = "${fieldConfig.comment}") + @ExcelProperty(value = "${fieldConfig.comment}") + private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; + + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Entity.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Entity.ftl new file mode 100644 index 0000000..ef77d8a --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Entity.ftl @@ -0,0 +1,38 @@ +package ${packageName}.${subPackageName}; + +import lombok.Data; + +import com.baomidou.mybatisplus.annotation.TableName; + +import top.wms.admin.common.model.entity.BaseDO; + +import java.io.Serial; +<#if hasTimeField> +import java.time.*; + +<#if hasBigDecimalField> +import java.math.BigDecimal; + + +/** + * ${businessName}实体 + * + * @author ${author} + * @since ${datetime} + */ +@Data +@TableName("${tableName}") +public class ${className} extends BaseDO { + + @Serial + private static final long serialVersionUID = 1L; +<#if fieldConfigs??> + <#list fieldConfigs as fieldConfig> + + /** + * ${fieldConfig.comment} + */ + private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; + + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Mapper.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Mapper.ftl new file mode 100644 index 0000000..63767c6 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Mapper.ftl @@ -0,0 +1,16 @@ +package ${packageName}.${subPackageName}; + +import top.continew.starter.data.mp.base.BaseMapper; +import ${packageName}.model.entity.${classNamePrefix}DO; +import org.springframework.stereotype.Repository; + +/** +* ${businessName} Mapper +* +* @author ${author} +* @since ${datetime} +*/ +@Repository +public interface ${className} extends BaseMapper<${classNamePrefix}DO> { + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/MapperXml.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/MapperXml.ftl new file mode 100644 index 0000000..a4d6605 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/MapperXml.ftl @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Menu.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Menu.ftl new file mode 100644 index 0000000..6560b76 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Menu.ftl @@ -0,0 +1,41 @@ +SET @parentId = ${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}; +-- ${businessName}管理菜单 +INSERT INTO `sys_menu` + (`id`, `title`, `parent_id`, `type`, `path`, `name`, `component`, `redirect`, `icon`, `is_external`, `is_cache`, `is_hidden`, `permission`, `sort`, `status`, `create_user`, `create_time`) +VALUES + (@parentId, '${businessName}管理', 1000, 2, '/${apiModuleName}/${apiName}', '${classNamePrefix}', '${apiModuleName}/${apiName}/index', NULL, NULL, b'0', b'0', b'0', NULL, 1, 1, 1, NOW()); + +-- ${businessName}管理按钮 +INSERT INTO `sys_menu` + (`id`, `title`, `parent_id`, `type`, `permission`, `sort`, `status`, `create_user`, `create_time`) +VALUES + (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '列表', @parentId, 3, '${apiModuleName}:${apiName}:list', 1, 1, 1, NOW()), + (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '详情', @parentId, 3, '${apiModuleName}:${apiName}:detail', 2, 1, 1, NOW()), + (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '新增', @parentId, 3, '${apiModuleName}:${apiName}:add', 3, 1, 1, NOW()), + (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '修改', @parentId, 3, '${apiModuleName}:${apiName}:update', 4, 1, 1, NOW()), + (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '删除', @parentId, 3, '${apiModuleName}:${apiName}:delete', 5, 1, 1, NOW()), + (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '导出', @parentId, 3, '${apiModuleName}:${apiName}:export', 6, 1, 1, NOW()); + +<#---- PostgreSQL(切换 PostgreSQL 数据库时请注释掉其他数据库脚本,并解开此段注释)--> +<#--DO $$--> +<#-- DECLARE sys_menu_id_seq INT8 := ${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c};--> +<#--BEGIN--> + +<#-- -- ${businessName}管理菜单--> +<#-- INSERT INTO "sys_menu"--> +<#-- ("id", "title", "parent_id", "type", "path", "name", "component", "redirect", "icon", "is_external", "is_cache", "is_hidden", "permission", "sort", "status", "create_user", "create_time")--> +<#-- VALUES--> +<#-- (sys_menu_id_seq, '${businessName}管理', 1000, 2, '/${apiModuleName}/${apiName}', '${classNamePrefix}', '${apiModuleName}/${apiName}/index', NULL, NULL, false, false, false, NULL, 1, 1, 1, NOW());--> + +<#-- -- ${businessName}管理按钮--> +<#-- INSERT INTO "sys_menu"--> +<#-- ("id", "title", "parent_id", "type", "permission", "sort", "status", "create_user", "create_time")--> +<#-- VALUES--> +<#-- (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '列表', sys_menu_id_seq, 3, '${apiModuleName}:${apiName}:list', 1, 1, 1, NOW()),--> +<#-- (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '详情', sys_menu_id_seq, 3, '${apiModuleName}:${apiName}:detail', 2, 1, 1, NOW()),--> +<#-- (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '新增', sys_menu_id_seq, 3, '${apiModuleName}:${apiName}:add', 3, 1, 1, NOW()),--> +<#-- (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '修改', sys_menu_id_seq, 3, '${apiModuleName}:${apiName}:update', 4, 1, 1, NOW()),--> +<#-- (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '删除', sys_menu_id_seq, 3, '${apiModuleName}:${apiName}:delete', 5, 1, 1, NOW()),--> +<#-- (${statics["cn.hutool.core.util.IdUtil"].getSnowflakeNextId()?c}, '导出', sys_menu_id_seq, 3, '${apiModuleName}:${apiName}:export', 6, 1, 1, NOW());--> + +<#--END $$;--> \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Query.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Query.ftl new file mode 100644 index 0000000..e871d2d --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Query.ftl @@ -0,0 +1,48 @@ +package ${packageName}.${subPackageName}; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import top.continew.starter.data.core.annotation.Query; +import top.continew.starter.data.core.enums.QueryType; + +import java.io.Serial; +import java.io.Serializable; +<#if hasTimeField> +import java.time.*; + +<#if hasBigDecimalField> +import java.math.BigDecimal; + + +/** + * ${businessName}查询条件 + * + * @author ${author} + * @since ${datetime} + */ +@Data +@Schema(description = "${businessName}查询条件") +public class ${className} implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +<#if fieldConfigs??> + <#list fieldConfigs as fieldConfig> + <#if fieldConfig.showInQuery> + + /** + * ${fieldConfig.comment} + */ + @Schema(description = "${fieldConfig.comment}") + @Query(type = QueryType.${fieldConfig.queryType}) + <#if fieldConfig.queryType = 'IN' || fieldConfig.queryType = 'NOT_IN' || fieldConfig.queryType = 'BETWEEN'> + private ${fieldConfig.fieldType}[] ${fieldConfig.fieldName}; + <#else> + private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; + + + + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Req.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Req.ftl new file mode 100644 index 0000000..975057f --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Req.ftl @@ -0,0 +1,56 @@ +package ${packageName}.${subPackageName}; + +<#if hasRequiredField> +import jakarta.validation.constraints.*; + + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; +<#if hasTimeField> +import java.time.*; + +<#if hasBigDecimalField> +import java.math.BigDecimal; + + +/** + * 创建或修改${businessName}参数 + * + * @author ${author} + * @since ${datetime} + */ +@Data +@Schema(description = "创建或修改${businessName}参数") +public class ${className} implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +<#if fieldConfigs??> + <#list fieldConfigs as fieldConfig> + <#if fieldConfig.showInForm> + + /** + * ${fieldConfig.comment} + */ + @Schema(description = "${fieldConfig.comment}") + <#if fieldConfig.isRequired> + <#if fieldConfig.fieldType = 'String'> + @NotBlank(message = "${fieldConfig.comment}不能为空") + <#else> + @NotNull(message = "${fieldConfig.comment}不能为空") + + + <#if fieldConfig.fieldType = 'String' && fieldConfig.columnSize??> + @Length(max = ${fieldConfig.columnSize?c}, message = "${fieldConfig.comment}长度不能超过 {max} 个字符") + + private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; + + + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Resp.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Resp.ftl new file mode 100644 index 0000000..223be21 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Resp.ftl @@ -0,0 +1,41 @@ +package ${packageName}.${subPackageName}; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; + +import top.wms.admin.common.model.resp.BaseDetailResp; + +import java.io.Serial; +<#if hasTimeField> +import java.time.*; + +<#if hasBigDecimalField> +import java.math.BigDecimal; + + +/** + * ${businessName}信息 + * + * @author ${author} + * @since ${datetime} + */ +@Data +@Schema(description = "${businessName}信息") +public class ${className} extends BaseDetailResp { + + @Serial + private static final long serialVersionUID = 1L; +<#if fieldConfigs??> + <#list fieldConfigs as fieldConfig> + <#if fieldConfig.showInList> + + /** + * ${fieldConfig.comment} + */ + @Schema(description = "${fieldConfig.comment}") + private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; + + + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Service.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Service.ftl new file mode 100644 index 0000000..f9ef77d --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/Service.ftl @@ -0,0 +1,17 @@ +package ${packageName}.${subPackageName}; + +import top.continew.starter.extension.crud.service.BaseService; +import ${packageName}.model.query.${classNamePrefix}Query; +import ${packageName}.model.req.${classNamePrefix}Req; +import ${packageName}.model.resp.${classNamePrefix}DetailResp; +import ${packageName}.model.resp.${classNamePrefix}Resp; + +/** + * ${businessName}业务接口 + * + * @author ${author} + * @since ${datetime} + */ +public interface ${className} extends BaseService<${classNamePrefix}Resp, ${classNamePrefix}Resp, ${classNamePrefix}Query, ${classNamePrefix}Req> { + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/ServiceImpl.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/ServiceImpl.ftl new file mode 100644 index 0000000..77d8178 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/backend/ServiceImpl.ftl @@ -0,0 +1,26 @@ +package ${packageName}.${subPackageName}; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +import top.continew.starter.extension.crud.service.BaseServiceImpl; +import ${packageName}.mapper.${classNamePrefix}Mapper; +import ${packageName}.model.entity.${classNamePrefix}DO; +import ${packageName}.model.query.${classNamePrefix}Query; +import ${packageName}.model.req.${classNamePrefix}Req; +import ${packageName}.model.resp.${classNamePrefix}DetailResp; +import ${packageName}.model.resp.${classNamePrefix}Resp; +import ${packageName}.service.${classNamePrefix}Service; + +/** + * ${businessName}业务实现 + * + * @author ${author} + * @since ${datetime} + */ +@Service +@RequiredArgsConstructor +public class ${className} extends BaseServiceImpl<${classNamePrefix}Mapper, ${classNamePrefix}DO, ${classNamePrefix}Resp, ${classNamePrefix}Resp, ${classNamePrefix}Query, ${classNamePrefix}Req> implements ${classNamePrefix}Service { + +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/AddModal.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/AddModal.ftl new file mode 100644 index 0000000..662c463 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/AddModal.ftl @@ -0,0 +1,141 @@ + + + + + diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/DetailDrawer.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/DetailDrawer.ftl new file mode 100644 index 0000000..97db30c --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/DetailDrawer.ftl @@ -0,0 +1,42 @@ + + + + + diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/api.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/api.ftl new file mode 100644 index 0000000..e737b87 --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/api.ftl @@ -0,0 +1,57 @@ +import http from '@/utils/http' + +const BASE_URL = '/${apiModuleName}/${apiName}' + +export interface ${classNamePrefix}Resp { +<#if fieldConfigs??> +<#list fieldConfigs as fieldConfig> + <#if fieldConfig.showInList> + ${fieldConfig.fieldName}: string + + + createUserString: string + updateUserString: string + disabled: boolean + +} +export interface ${classNamePrefix}Query { +<#if fieldConfigs??> +<#list fieldConfigs as fieldConfig> + <#if fieldConfig.showInQuery> + ${fieldConfig.fieldName}: string | undefined + + + + sort: Array +} +export interface ${classNamePrefix}PageQuery extends ${classNamePrefix}Query, PageQuery {} + +/** @desc 查询${businessName}列表 */ +export function list${classNamePrefix}(query: ${classNamePrefix}PageQuery) { + return http.get>(`${'$'}{BASE_URL}`, query) +} + +/** @desc 查询${businessName}详情 */ +export function get${classNamePrefix}(id: string) { + return http.get<${classNamePrefix}DetailResp>(`${'$'}{BASE_URL}/${'$'}{id}`) +} + +/** @desc 新增${businessName} */ +export function add${classNamePrefix}(data: any) { + return http.post(`${'$'}{BASE_URL}`, data) +} + +/** @desc 修改${businessName} */ +export function update${classNamePrefix}(data: any, id: string) { + return http.put(`${'$'}{BASE_URL}/${'$'}{id}`, data) +} + +/** @desc 删除${businessName} */ +export function delete${classNamePrefix}(id: string) { + return http.del(`${'$'}{BASE_URL}/${'$'}{id}`) +} + +/** @desc 导出${businessName} */ +export function export${classNamePrefix}(query: ${classNamePrefix}Query) { + return http.download(`${'$'}{BASE_URL}/export`, query) +} diff --git a/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/index.ftl b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/index.ftl new file mode 100644 index 0000000..a05031a --- /dev/null +++ b/mes-plugin/mes-plugin-generator/src/main/resources/templates/frontend/index.ftl @@ -0,0 +1,202 @@ + + + + + diff --git a/mes-plugin/mes-plugin-schedule/pom.xml b/mes-plugin/mes-plugin-schedule/pom.xml new file mode 100644 index 0000000..d623f26 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + top.wms + wms-plugin + ${revision} + + + + wms-plugin-schedule + 任务调度插件 + + + + + com.aizuda + snail-job-client-starter + + + com.aizuda + snail-job-client-retry-core + + + com.aizuda + snail-job-client-job-core + + + + + org.springframework + spring-webflux + + + + io.projectreactor.netty + reactor-netty-http + + + diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobApi.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobApi.java new file mode 100644 index 0000000..5220a14 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobApi.java @@ -0,0 +1,97 @@ +package top.wms.admin.schedule.api; + +import com.aizuda.snailjob.common.core.model.Result; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.*; +import top.wms.admin.schedule.model.JobPageResult; +import top.wms.admin.schedule.model.req.JobReq; +import top.wms.admin.schedule.model.req.JobStatusReq; +import top.wms.admin.schedule.model.resp.JobResp; + +import java.util.List; +import java.util.Set; + +/** + * 任务 REST API + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 18:20 + */ +@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE) +public interface JobApi { + + /** + * 分页查询列表 + * + * @param groupName 任务组 + * @param jobName 任务名称 + * @param jobStatus 任务状态 + * @param page 页码 + * @param size 每页条数 + * @return 响应信息 + */ + @GetExchange("/job/page/list") + ResponseEntity>> page(@RequestParam(value = "groupName", required = false) String groupName, + @RequestParam(value = "jobName", required = false) String jobName, + @RequestParam(value = "jobStatus", required = false) Integer jobStatus, + @RequestParam("page") int page, + @RequestParam("size") int size); + + /** + * 新增 + * + * @param req 新增信息 + * @return 响应信息 + */ + @PostExchange("/job") + ResponseEntity> add(@RequestBody JobReq req); + + /** + * 修改 + * + * @param req 修改信息 + * @return 响应信息 + */ + @PutExchange("/job") + ResponseEntity> update(@RequestBody JobReq req); + + /** + * 修改状态 + * + * @param req 修改信息 + * @return 响应信息 + */ + @PutExchange("/job/status") + ResponseEntity> updateStatus(@RequestBody JobStatusReq req); + + /** + * 删除 + * + * @param ids ID 列表 + * @return 响应信息 + */ + @DeleteExchange("/job/ids") + ResponseEntity> delete(@RequestBody Set ids); + + /** + * 执行 + * + * @param id ID + * @return 响应信息 + */ + @PostExchange("/job/trigger/{id}") + ResponseEntity> trigger(@PathVariable("id") Long id); + + /** + * 查询分组列表 + * + * @return 响应信息 + */ + @GetExchange("/group/all/group-name/list") + ResponseEntity>> listGroup(); +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobBatchApi.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobBatchApi.java new file mode 100644 index 0000000..6e26191 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobBatchApi.java @@ -0,0 +1,100 @@ +package top.wms.admin.schedule.api; + +import com.aizuda.snailjob.common.core.model.Result; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; +import top.wms.admin.schedule.model.JobInstanceLogPageResult; +import top.wms.admin.schedule.model.JobPageResult; +import top.wms.admin.schedule.model.resp.JobInstanceResp; +import top.wms.admin.schedule.model.resp.JobLogResp; + +import java.util.List; + +/** + * 任务批次 REST API + * + * @author KAI + * @author Charles7c + * @since 2024/6/27 23:03 + */ +@HttpExchange(value = "/job", accept = MediaType.APPLICATION_JSON_VALUE) +public interface JobBatchApi { + + /** + * 分页查询列表 + * + * @param jobId 任务 ID + * @param jobName 任务名称 + * @param groupName 组名称 + * @param taskBatchStatus 任务批次状态 + * @param datetimeRange 时间范围 + * @param page 页码 + * @param size 每页条数 + * @return 响应信息 + */ + @GetExchange("/batch/list") + ResponseEntity>> page(@RequestParam(value = "jobId", required = false) Long jobId, + @RequestParam(value = "jobName", required = false) String jobName, + @RequestParam(value = "groupName", required = false) String groupName, + @RequestParam(value = "taskBatchStatus", required = false) Integer taskBatchStatus, + @RequestParam(value = "datetimeRange", required = false) String[] datetimeRange, + @RequestParam(value = "page") Integer page, + @RequestParam(value = "size") Integer size); + + /** + * 停止 + * + * @param id ID + * @return 响应信息 + */ + @PostExchange("/batch/stop/{id}") + ResponseEntity> stop(@PathVariable("id") Long id); + + /** + * 重试 + * + * @param id ID + * @return 响应信息 + */ + @PostExchange("/batch/retry/{id}") + ResponseEntity> retry(@PathVariable("id") Long id); + + /** + * 分页查询任务实例列表 + * + * @param jobId 任务 ID + * @param taskBatchId 任务批次 ID + * @param page 页码 + * @param size 每页条数 + * @return 响应信息 + */ + @GetExchange("/task/list") + ResponseEntity>> pageTask(@RequestParam(value = "jobId", required = false) Long jobId, + @RequestParam(value = "taskBatchId") Long taskBatchId, + @RequestParam(value = "page") Integer page, + @RequestParam(value = "size") Integer size); + + /** + * 分页查询任务实例日志列表 + * + * @param jobId 任务 ID + * @param taskBatchId 任务批次 ID + * @param taskId 任务实例ID + * @param startId 起始 ID + * @param fromIndex 起始索引 + * @param size 每页条数 + * @return 响应信息 + */ + @GetExchange("/log/list") + ResponseEntity> pageLog(@RequestParam(value = "jobId", required = false) Long jobId, + @RequestParam(value = "taskBatchId") Long taskBatchId, + @RequestParam(value = "taskId") Long taskId, + @RequestParam(value = "startId") Integer startId, + @RequestParam(value = "fromIndex") Integer fromIndex, + @RequestParam(value = "size") Integer size); +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobClient.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobClient.java new file mode 100644 index 0000000..77f6351 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/api/JobClient.java @@ -0,0 +1,141 @@ +package top.wms.admin.schedule.api; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import cn.hutool.jwt.JWTUtil; +import cn.hutool.jwt.RegisteredPayload; +import com.aizuda.snailjob.common.core.model.Result; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import top.wms.admin.schedule.constant.JobConstants; +import top.wms.admin.schedule.model.JobPageResult; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * 任务调度客户端 + * + * @author Charles7c + * @since 2024/7/4 23:07 + */ +@Slf4j +@Data +public class JobClient { + + public static final Integer STATUS_SUCCESS = 1; + private static final String AUTH_URL = "/auth/login"; + private final String url; + private final String username; + private final String password; + + public JobClient(String url, String username, String password) { + Assert.notBlank(url, "任务调度中心 URL 不能为空"); + Assert.notBlank(username, "任务调度中心用户名不能为空"); + Assert.notBlank(password, "任务调度中心密码不能为空"); + this.url = url; + this.username = username; + this.password = password; + } + + /** + * 请求 + * + * @param apiSupplier API 请求 + * @param 响应类型 + * @return 响应信息 + */ + public T request(Supplier>> apiSupplier) { + ResponseEntity> responseEntity = apiSupplier.get(); + this.checkResponse(responseEntity); + Result result = responseEntity.getBody(); + if (!STATUS_SUCCESS.equals(result.getStatus())) { + throw new IllegalStateException(result.getMessage()); + } + return result.getData(); + } + + /** + * 分页请求 + * + * @param apiSupplier API 请求 + * @param 响应类型 + * @return 分页列表信息 + */ + public PageResp requestPage(Supplier>>> apiSupplier) { + ResponseEntity>> responseEntity = apiSupplier.get(); + this.checkResponse(responseEntity); + JobPageResult> result = responseEntity.getBody(); + if (!STATUS_SUCCESS.equals(result.getStatus())) { + throw new IllegalStateException(result.getMessage()); + } + PageResp page = new PageResp<>(); + page.setList(result.getData()); + page.setTotal(result.getTotal()); + return page; + } + + /** + * 获取 Token + * + * @return Token + */ + public String getToken() { + String token = RedisUtils.get(JobConstants.AUTH_TOKEN_HEADER); + if (StrUtil.isBlank(token)) { + token = this.authenticate(); + Object expiresAtSeconds = JWTUtil.parseToken(token).getPayload(RegisteredPayload.EXPIRES_AT); + RedisUtils.set(JobConstants.AUTH_TOKEN_HEADER, token, Duration.ofSeconds(Convert + .toLong(expiresAtSeconds) - DateUtil.currentSeconds() - 60)); + } + return token; + } + + /** + * 密码认证 + * + * @return Token + */ + private String authenticate() { + Map paramMap = MapUtil.newHashMap(2); + paramMap.put("username", username); + paramMap.put("password", SecureUtil.md5(password)); + HttpRequest httpRequest = HttpUtil.createPost("%s%s".formatted(url, AUTH_URL)); + httpRequest.body(JSONUtil.toJsonStr(paramMap)); + HttpResponse response = httpRequest.execute(); + if (!response.isOk() || response.body() == null) { + throw new IllegalStateException("连接任务调度中心异常"); + } + Result result = JSONUtil.toBean(response.body(), Result.class); + if (!STATUS_SUCCESS.equals(result.getStatus())) { + log.warn("Password Authentication failed, expected a successful response. error msg: {}", result + .getMessage()); + throw new IllegalStateException(result.getMessage()); + } + return JSONUtil.parseObj(result.getData()).getStr("token"); + } + + /** + * 检查响应 + * + * @param responseEntity 响应信息 + */ + private void checkResponse(ResponseEntity responseEntity) { + if (!responseEntity.getStatusCode().is2xxSuccessful() || responseEntity.getBody() == null) { + throw new IllegalStateException("连接任务调度中心异常"); + } + } +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/config/HttpExchangeConfiguration.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/config/HttpExchangeConfiguration.java new file mode 100644 index 0000000..c6a39a6 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/config/HttpExchangeConfiguration.java @@ -0,0 +1,118 @@ +package top.wms.admin.schedule.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.support.WebClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import top.wms.admin.schedule.api.JobApi; +import top.wms.admin.schedule.api.JobBatchApi; +import top.wms.admin.schedule.api.JobClient; +import top.wms.admin.schedule.constant.JobConstants; + +/** + * HTTP Exchange 配置 + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 18:03 + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class HttpExchangeConfiguration { + + private final ObjectMapper objectMapper; + @Value("${snail-job.namespace}") + private String namespace; + + @Value("${snail-job.server.api.url}") + private String baseUrl; + + @Value("${snail-job.server.api.username}") + private String username; + + @Value("${snail-job.server.api.password}") + private String password; + + @Bean + public JobApi jobApi() { + return httpServiceProxyFactory().createClient(JobApi.class); + } + + @Bean + public JobBatchApi jobBatchApi() { + return httpServiceProxyFactory().createClient(JobBatchApi.class); + } + + @Bean + public HttpServiceProxyFactory httpServiceProxyFactory() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) + .doOnConnected(conn -> { + conn.addHandlerLast(new ReadTimeoutHandler(10)); + conn.addHandlerLast(new WriteTimeoutHandler(10)); + }) + .wiretap(true); + + WebClient webClient = WebClient.builder() + .codecs(config -> config.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper))) + .codecs(config -> config.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper))) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .filter(logRequest()) + .filter(logResponse()) + .filter((request, next) -> { + // 设置请求头 + ClientRequest filtered = ClientRequest.from(request) + .header(JobConstants.NAMESPACE_ID_HEADER, namespace) + .header(JobConstants.AUTH_TOKEN_HEADER, jobClient().getToken()) + .build(); + return next.exchange(filtered); + }) + .baseUrl(baseUrl) + .build(); + return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)).build(); + } + + @Bean + public JobClient jobClient() { + return new JobClient(baseUrl, username, password); + } + + /** + * 打印请求日志 + */ + private ExchangeFilterFunction logRequest() { + return ExchangeFilterFunction.ofRequestProcessor(request -> { + log.info("---> {} {}", request.method(), request.url()); + return Mono.just(request); + }); + } + + /** + * 打印响应日志 + */ + private ExchangeFilterFunction logResponse() { + return ExchangeFilterFunction.ofResponseProcessor(response -> response.bodyToMono(String.class) + .flatMap(body -> { + log.info("<--- {}", response.statusCode()); + log.info(body); + return Mono.just(ClientResponse.from(response).body(body).build()); + })); + } +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/config/SnailJobConfiguration.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/config/SnailJobConfiguration.java new file mode 100644 index 0000000..3f6b816 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/config/SnailJobConfiguration.java @@ -0,0 +1,36 @@ +package top.wms.admin.schedule.config; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.aizuda.snailjob.client.common.appender.SnailLogbackAppender; +import com.aizuda.snailjob.client.common.event.SnailClientStartingEvent; +import com.aizuda.snailjob.client.starter.EnableSnailJob; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +/** + * Snail Job 配置 + * + * @author KAI + * @since 2024/6/26 9:19 + */ +@Configuration +@EnableSnailJob +@ConditionalOnProperty(prefix = "snail-job", value = "enabled", havingValue = "true", matchIfMissing = true) +public class SnailJobConfiguration { + + /** + * 日志上报 + */ + @EventListener(SnailClientStartingEvent.class) + public void onStarting() { + SnailLogbackAppender appender = new SnailLogbackAppender<>(); + appender.start(); + LoggerContext loggerContext = (LoggerContext)LoggerFactory.getILoggerFactory(); + Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.addAppender(appender); + } +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/constant/JobConstants.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/constant/JobConstants.java new file mode 100644 index 0000000..14b90bf --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/constant/JobConstants.java @@ -0,0 +1,23 @@ +package top.wms.admin.schedule.constant; + +/** + * 任务调度常量 + * + * @author KAI + * @since 2024/6/26 9:19 + */ +public class JobConstants { + + /** + * 请求头:命名空间 ID + */ + public static final String NAMESPACE_ID_HEADER = "SNAIL-JOB-NAMESPACE-ID"; + + /** + * 请求头:认证令牌 + */ + public static final String AUTH_TOKEN_HEADER = "Snail-Job-Auth"; + + private JobConstants() { + } +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobBlockStrategyEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobBlockStrategyEnum.java new file mode 100644 index 0000000..b9c183f --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobBlockStrategyEnum.java @@ -0,0 +1,34 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务阻塞策略枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobBlockStrategyEnum implements BaseEnum { + + /** + * 丢弃 + */ + DISCARD(1, "丢弃"), + + /** + * 覆盖 + */ + COVER(2, "覆盖"), + + /** + * 并行 + */ + PARALLEL(3, "并行"),; + + private final Integer value; + private final String description; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobExecuteReasonEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobExecuteReasonEnum.java new file mode 100644 index 0000000..1e17d15 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobExecuteReasonEnum.java @@ -0,0 +1,119 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务执行原因枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobExecuteReasonEnum implements BaseEnum { + + /** + * 无 + */ + NONE(0, "无"), + + /** + * 任务执行超时 + */ + TIME_OUT(1, "任务执行超时"), + + /** + * 无客户端节点 + */ + CLIENT_NOT_FOUND(2, "无客户端节点"), + + /** + * 任务已关闭 + */ + TASK_CLOSED(3, "任务已关闭"), + + /** + * 任务丢弃 + */ + TASK_DROPPED(4, "任务丢弃"), + + /** + * 任务被覆盖 + */ + TASK_COVERED(5, "任务被覆盖"), + + /** + * 无可执行任务项 + */ + TASK_NONE(6, "无可执行任务项"), + + /** + * 任务执行期间发生非预期异常 + */ + TASK_EXCEPTION(7, "任务执行期间发生非预期异常"), + + /** + * 手动停止 + */ + MANUAL_STOP(8, "手动停止"), + + /** + * 条件节点执行异常 + */ + NODE_EXCEPTION(9, "条件节点执行异常"), + + /** + * 任务中断 + */ + TASK_INTERRUPT(10, "任务中断"), + + /** + * 回调节点执行异常 + */ + CALLBACK_EXCEPTION(11, "回调节点执行异常"), + + /** + * 无需处理 + */ + NO_NEED_PROCESS(12, "无需处理"), + + /** + * 节点关闭跳过执行 + */ + NODE_SKIP(13, "节点关闭跳过执行"), + + /** + * 判定未通过 + */ + NOT_PASS(14, "判定未通过"), + + /** + * 任务已完成 + */ + TASK_FINISHED(15, "任务已完成"), + + /** + * 任务状态 + */ + TASK_RUNNING(16, "任务正在执行"), + + /** + * 任务等待执行 + */ + TASK_WAITING(17, "任务等待执行"), + + /** + * 任务执行失败 + */ + TASK_FAILED(18, "任务执行失败"), + + /** + * 任务执行成功 + */ + TASK_SUCCESS(19, "任务执行成功"),; + + private final Integer value; + private final String description; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobExecuteStatusEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobExecuteStatusEnum.java new file mode 100644 index 0000000..1bf9fe5 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobExecuteStatusEnum.java @@ -0,0 +1,51 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务执行状态枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobExecuteStatusEnum implements BaseEnum { + + /** + * 待处理 + */ + WAITING(1, "待处理", UiConstants.COLOR_PRIMARY), + + /** + * 运行中 + */ + RUNNING(2, "运行中", UiConstants.COLOR_WARNING), + + /** + * 成功 + */ + SUCCEEDED(3, "成功", UiConstants.COLOR_SUCCESS), + + /** + * 已失败 + */ + FAILED(4, "已失败", UiConstants.COLOR_ERROR), + + /** + * 已停止 + */ + STOPPED(5, "已停止", UiConstants.COLOR_ERROR), + + /** + * 已取消 + */ + CANCELED(6, "已取消", UiConstants.COLOR_DEFAULT),; + + private final Integer value; + private final String description; + private final String color; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobRouteStrategyEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobRouteStrategyEnum.java new file mode 100644 index 0000000..54a3c83 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobRouteStrategyEnum.java @@ -0,0 +1,39 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务路由策略枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobRouteStrategyEnum implements BaseEnum { + + /** + * 轮询 + */ + POLLING(4, "轮询"), + + /** + * 随机 + */ + RANDOM(2, "随机"), + + /** + * 一致性哈希 + */ + HASH(1, "一致性哈希"), + + /** + * LRU + */ + LRU(3, "LRU"),; + + private final Integer value; + private final String description; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobStatusEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobStatusEnum.java new file mode 100644 index 0000000..1d5057c --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobStatusEnum.java @@ -0,0 +1,31 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务状态枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobStatusEnum implements BaseEnum { + + /** + * 禁用 + */ + DISABLED(0, "禁用", UiConstants.COLOR_ERROR), + + /** + * 启用 + */ + ENABLED(1, "启用", UiConstants.COLOR_SUCCESS),; + + private final Integer value; + private final String description; + private final String color; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobTaskTypeEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobTaskTypeEnum.java new file mode 100644 index 0000000..edeea9b --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobTaskTypeEnum.java @@ -0,0 +1,36 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.wms.admin.common.constant.UiConstants; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务类型枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobTaskTypeEnum implements BaseEnum { + + /** + * 集群 + */ + CLUSTER(1, "集群", UiConstants.COLOR_PRIMARY), + + /** + * 广播 + */ + BROADCAST(2, "广播", UiConstants.COLOR_PRIMARY), + + /** + * 静态切片 + */ + SLICE(3, "静态切片", UiConstants.COLOR_PRIMARY),; + + private final Integer value; + private final String description; + private final String color; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobTriggerTypeEnum.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobTriggerTypeEnum.java new file mode 100644 index 0000000..0d988f3 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/enums/JobTriggerTypeEnum.java @@ -0,0 +1,29 @@ +package top.wms.admin.schedule.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 任务触发类型枚举 + * + * @author Charles7c + * @since 2024/7/11 22:28 + */ +@Getter +@RequiredArgsConstructor +public enum JobTriggerTypeEnum implements BaseEnum { + + /** + * 固定时间 + */ + FIXED_TIME(2, "固定时间"), + + /** + * CRON + */ + CRON(3, "CRON"); + + private final Integer value; + private final String description; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/JobInstanceLogPageResult.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/JobInstanceLogPageResult.java new file mode 100644 index 0000000..31115bc --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/JobInstanceLogPageResult.java @@ -0,0 +1,58 @@ +package top.wms.admin.schedule.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 任务实例日志分页信息 + * + * @author Charles7c + * @since 2024/7/14 21:51 + */ +@Data +@Schema(description = "任务实例日志分页信息") +public class JobInstanceLogPageResult implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 日志详情 + */ + @Schema(description = "日志详情") + private List message; + + /** + * 异常信息 + */ + @Schema(description = "异常信息") + private String throwable; + + /** + * 是否结束 + */ + @Schema(description = "是否结束", example = "true") + private boolean isFinished; + + /** + * 起始索引 + */ + @Schema(description = "起始索引", example = "0") + private Integer fromIndex; + + /** + * 下一个开始 ID + */ + @Schema(description = "下一个开始ID", example = "9") + private Long nextStartId; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/JobPageResult.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/JobPageResult.java new file mode 100644 index 0000000..520c6ca --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/JobPageResult.java @@ -0,0 +1,30 @@ +package top.wms.admin.schedule.model; + +import com.aizuda.snailjob.common.core.model.Result; +import lombok.Data; + +/** + * 任务调度服务端分页返回对象 + * + * @author KAI + * @author Charles7c + * @since 2024/6/26 22:27 + */ +@Data +public class JobPageResult extends Result { + + /** + * 页码 + */ + private long page; + + /** + * 每页条数 + */ + private long size; + + /** + * 总条数 + */ + private long total; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobInstanceLogQuery.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobInstanceLogQuery.java new file mode 100644 index 0000000..7e1008f --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobInstanceLogQuery.java @@ -0,0 +1,61 @@ +package top.wms.admin.schedule.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 任务实例日志查询条件 + * + * @author KAI + * @since 2024/6/28 16:58 + */ +@Data +@Schema(description = "任务实例日志查询条件") +public class JobInstanceLogQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务 ID + */ + @Schema(description = "任务ID", example = "1") + private Long jobId; + + /** + * 任务批次 ID + */ + @Schema(description = "任务批次ID", example = "1") + private Long taskBatchId; + + /** + * 任务实例 ID + */ + @Schema(description = "任务实例ID", example = "1") + private Long taskId; + + /** + * 开始 ID + */ + @Schema(description = "开始ID", example = "2850") + private Integer startId; + + /** + * 起始索引 + */ + @Schema(description = "起始索引", example = "0") + @Min(value = 0, message = "起始索引最小值为 {value}") + private Integer fromIndex = 0; + + /** + * 每页条数 + */ + @Schema(description = "每页条数", example = "50") + @Range(min = 1, max = 1000, message = "每页条数(取值范围 {min}-{max})") + private Integer size = 50; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobInstanceQuery.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobInstanceQuery.java new file mode 100644 index 0000000..63979f3 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobInstanceQuery.java @@ -0,0 +1,33 @@ +package top.wms.admin.schedule.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 任务实例查询条件 + * + * @author KAI + * @since 2024/6/28 16:58 + */ +@Data +@Schema(description = "任务实例查询条件") +public class JobInstanceQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务 ID + */ + @Schema(description = "任务ID", example = "1") + private Long jobId; + + /** + * 任务批次 ID + */ + @Schema(description = "任务批次ID", example = "1") + private Long taskBatchId; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobLogQuery.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobLogQuery.java new file mode 100644 index 0000000..d50dfed --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobLogQuery.java @@ -0,0 +1,74 @@ +package top.wms.admin.schedule.model.query; + +import cn.hutool.core.date.DatePattern; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.hibernate.validator.constraints.Range; +import org.springframework.format.annotation.DateTimeFormat; +import top.wms.admin.schedule.enums.JobExecuteStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 任务日志查询条件 + * + * @author KAI + * @since 2024/6/27 23:58 + */ +@Data +@Schema(description = "任务日志查询条件") +public class JobLogQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务 ID + */ + @Schema(description = "任务ID", example = "1") + private Long jobId; + + /** + * 任务组 + */ + @Schema(description = "任务组", example = "wms-admin") + private String groupName; + + /** + * 任务名称 + */ + @Schema(description = "任务名称", example = "定时任务1") + private String jobName; + + /** + * 任务批次状态 + */ + @Schema(description = "任务批次状态", example = "1") + private JobExecuteStatusEnum taskBatchStatus; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 00:00:00,2023-08-08 23:59:59") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @Size(max = 2, message = "创建时间必须是一个范围") + private LocalDateTime[] datetimeRange; + + /** + * 页码 + */ + @Schema(description = "页码", example = "1") + @Min(value = 1, message = "页码最小值为 {value}") + private Integer page = 1; + + /** + * 每页条数 + */ + @Schema(description = "每页条数", example = "10") + @Range(min = 1, max = 1000, message = "每页条数(取值范围 {min}-{max})") + private Integer size = 10; +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobQuery.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobQuery.java new file mode 100644 index 0000000..6b0adc9 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/query/JobQuery.java @@ -0,0 +1,56 @@ +package top.wms.admin.schedule.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import lombok.Data; +import org.hibernate.validator.constraints.Range; +import top.wms.admin.schedule.enums.JobStatusEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 任务查询条件 + * + * @author KAI + * @since 2024/6/25 16:43 + */ +@Data +@Schema(description = "任务查询条件") +public class JobQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务组 + */ + @Schema(description = "任务组", example = "wms-admin") + private String groupName; + + /** + * 任务名称 + */ + @Schema(description = "任务名称", example = "定时任务1") + private String jobName; + + /** + * 任务状态 + */ + @Schema(description = "任务状态", example = "1") + private JobStatusEnum jobStatus; + + /** + * 页码 + */ + @Schema(description = "页码", example = "1") + @Min(value = 1, message = "页码最小值为 {value}") + private Integer page = 1; + + /** + * 每页条数 + */ + @Schema(description = "每页条数", example = "10") + @Range(min = 1, max = 1000, message = "每页条数(取值范围 {min}-{max})") + private Integer size = 10; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/req/JobReq.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/req/JobReq.java new file mode 100644 index 0000000..6fcfe1c --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/req/JobReq.java @@ -0,0 +1,147 @@ +package top.wms.admin.schedule.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import top.wms.admin.schedule.enums.*; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 创建或修改任务参数 + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 16:40 + */ +@Data +@Schema(description = "创建或修改任务参数") +public class JobReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务组 + */ + @Schema(description = "任务组", example = "wms-admin") + @NotBlank(message = "任务组不能为空") + private String groupName; + + /** + * 任务名称 + */ + @Schema(description = "任务名称", example = "定时任务1") + @NotBlank(message = "任务名称不能为空") + @Length(max = 64, message = "任务名称不能超过 {max} 个字符") + private String jobName; + + /** + * 描述 + */ + @Schema(description = "描述", example = "定时任务1的描述") + private String description; + + /** + * 触发类型 + */ + @Schema(description = "触发类型", example = "2") + @NotNull(message = "触发类型非法") + private JobTriggerTypeEnum triggerType; + + /** + * 间隔时长 + */ + @Schema(description = "间隔时长", example = "60") + @NotBlank(message = "间隔时长不能为空") + private String triggerInterval; + + /** + * 执行器类型 + */ + @Schema(description = "执行器类型", example = "1", defaultValue = "1") + private Integer executorType = 1; + + /** + * 任务类型 + */ + @Schema(description = "任务类型", example = "1") + @NotNull(message = "任务类型非法") + private JobTaskTypeEnum taskType; + + /** + * 执行器名称 + */ + @Schema(description = "执行器名称", example = "test") + @NotBlank(message = "执行器名称不能为空") + private String executorInfo; + + /** + * 任务参数 + */ + @Schema(description = "任务参数", example = "") + private String argsStr; + + /** + * 参数类型 + */ + @Schema(description = "参数类型", example = "1") + private Integer argsType; + + /** + * 路由策略 + */ + @Schema(description = "路由策略", example = "4") + @NotNull(message = "路由策略非法") + private JobRouteStrategyEnum routeKey; + + /** + * 阻塞策略 + */ + @Schema(description = "阻塞策略", example = "1") + @NotNull(message = "阻塞策略非法") + private JobBlockStrategyEnum blockStrategy; + + /** + * 超时时间(单位:秒) + */ + @Schema(description = "超时时间(单位:秒)", example = "60") + @NotNull(message = "超时时间不能为空") + private Integer executorTimeout; + + /** + * 最大重试次数 + */ + @Schema(description = "最大重试次数", example = "3") + @NotNull(message = "最大重试次数不能为空") + private Integer maxRetryTimes; + + /** + * 重试间隔(单位:秒) + */ + @Schema(description = "重试间隔(单位:秒)", example = "1") + @NotNull(message = "重试间隔不能为空") + private Integer retryInterval; + + /** + * 并行数 + */ + @Schema(description = "并行数", example = "1") + @NotNull(message = "并行数不能为空") + private Integer parallelNum; + + /** + * 任务状态 + */ + @Schema(description = "任务状态", example = "0", defaultValue = "0") + private JobStatusEnum jobStatus = JobStatusEnum.DISABLED; + + /** + * ID + */ + @Schema(hidden = true) + private Long id; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/req/JobStatusReq.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/req/JobStatusReq.java new file mode 100644 index 0000000..5a069d4 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/req/JobStatusReq.java @@ -0,0 +1,37 @@ +package top.wms.admin.schedule.model.req; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import top.wms.admin.schedule.enums.JobStatusEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 修改任务状态信息 + * + * @author KAI + * @author Charles7c + * @since 2024/6/27 9:24 + */ +@Data +@Schema(description = "修改任务状态信息") +public class JobStatusReq implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 任务状态 + */ + @Schema(description = "任务状态", example = "1") + @NotNull(message = "任务状态非法") + private JobStatusEnum jobStatus; + + /** + * ID + */ + @Schema(hidden = true) + private Long id; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobInstanceResp.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobInstanceResp.java new file mode 100644 index 0000000..525f567 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobInstanceResp.java @@ -0,0 +1,70 @@ +package top.wms.admin.schedule.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 任务实例信息 + * + * @author KAI + * @author Charles7c + * @since 2024/6/28 16:58 + */ +@Data +@Schema(description = "任务实例信息") +public class JobInstanceResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 任务组 + */ + @Schema(description = "任务组", example = "wms-admin") + private String groupName; + + /** + * 任务 ID + */ + @Schema(description = "任务ID", example = "1") + private Long jobId; + + /** + * 任务批次 ID + */ + @Schema(description = "任务批次ID", example = "1") + private Long taskBatchId; + + /** + * 执行状态 + */ + @Schema(description = "执行状态", example = "1") + private Integer taskStatus; + + /** + * 重试次数 + */ + @Schema(description = "重试次数", example = "1") + private Integer retryCount; + + /** + * 执行结果 + */ + @Schema(description = "执行结果", example = "") + private String resultMessage; + + /** + * 客户端信息 + */ + @Schema(description = "客户端信息", example = "1812406095098114048@192.168.138.48:1789") + private String clientInfo; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobLogResp.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobLogResp.java new file mode 100644 index 0000000..e12a145 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobLogResp.java @@ -0,0 +1,85 @@ +package top.wms.admin.schedule.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.schedule.enums.JobExecuteReasonEnum; +import top.wms.admin.schedule.enums.JobExecuteStatusEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 任务日志信息 + * + * @author KAI + * @author Charles7c + * @since 2024/6/27 22:50 + */ +@Data +@Schema(description = "任务日志信息") +public class JobLogResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 任务组 + */ + @Schema(description = "任务组", example = "wms-admin") + private String groupName; + + /** + * 任务名称 + */ + @Schema(description = "任务名称", example = "定时任务1") + private String jobName; + + /** + * 任务 ID + */ + @Schema(description = "任务ID", example = "1") + private Long jobId; + + /** + * 任务状态 + */ + @Schema(description = "任务状态", example = "3") + private JobExecuteStatusEnum taskBatchStatus; + + /** + * 操作原因 + */ + @Schema(description = "操作原因", example = "0") + private JobExecuteReasonEnum operationReason; + + /** + * 执行器类型 + */ + @Schema(description = "执行器类型", example = "1") + private Integer executorType; + + /** + * 执行器名称 + */ + @Schema(description = "执行器名称", example = "test") + private String executorInfo; + + /** + * 执行时间 + */ + @Schema(description = "执行时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime executionAt; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:08", type = "string") + private LocalDateTime createDt; +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobResp.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobResp.java new file mode 100644 index 0000000..10a73be --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/model/resp/JobResp.java @@ -0,0 +1,150 @@ +package top.wms.admin.schedule.model.resp; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import top.wms.admin.schedule.enums.*; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 任务信息 + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 17:15 + */ +@Data +@Schema(description = "任务信息") +public class JobResp implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID", example = "1") + private Long id; + + /** + * 任务组 + */ + @Schema(description = "任务组", example = "wms-admin") + private String groupName; + + /** + * 任务名称 + */ + @Schema(description = "任务名称", example = "定时任务1") + private String jobName; + + /** + * 描述 + */ + @Schema(description = "描述", example = "定时任务1的描述") + private String description; + + /** + * 触发类型 + */ + @Schema(description = "触发类型", example = "2") + private JobTriggerTypeEnum triggerType; + + /** + * 间隔时长 + */ + @Schema(description = "间隔时长", example = "60") + private String triggerInterval; + + /** + * 执行器类型 + */ + @Schema(description = " 执行器类型", example = "1") + private Integer executorType; + + /** + * 任务类型 + */ + @Schema(description = "任务类型", example = "1") + private JobTaskTypeEnum taskType; + + /** + * 执行器名称 + */ + @Schema(description = "执行器名称", example = "test") + private String executorInfo; + + /** + * 任务参数 + */ + @Schema(description = "任务参数", example = "") + private String argsStr; + + /** + * 参数类型 + */ + @Schema(description = "参数类型", example = "1") + private String argsType; + + /** + * 路由策略 + */ + @Schema(description = "路由策略", example = "1") + private JobRouteStrategyEnum routeKey; + + /** + * 阻塞策略 + */ + @Schema(description = "阻塞策略", example = "1") + private JobBlockStrategyEnum blockStrategy; + + /** + * 超时时间(单位:秒) + */ + @Schema(description = "超时时间(单位:秒)", example = "60") + private Integer executorTimeout; + + /** + * 最大重试次数 + */ + @Schema(description = "最大重试次数", example = "3") + private Integer maxRetryTimes; + + /** + * 重试间隔(单位:秒) + */ + @Schema(description = "重试间隔", example = "1") + private Integer retryInterval; + + /** + * 并行数 + */ + @Schema(description = "并行数", example = "1") + private Integer parallelNum; + + /** + * 任务状态 + */ + @Schema(description = "任务状态", example = "1") + private JobStatusEnum jobStatus; + + /** + * 下次触发时间 + */ + @Schema(description = "下次触发时间", example = "2023-08-08 08:09:00", type = "string") + private LocalDateTime nextTriggerAt; + + /** + * 创建时间 + */ + @Schema(description = "创建时间", example = "2023-08-08 08:08:00", type = "string") + private LocalDateTime createDt; + + /** + * 修改时间 + */ + @Schema(description = "修改时间", example = "2023-08-08 08:08:00", type = "string") + private LocalDateTime updateDt; +} \ No newline at end of file diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/JobLogService.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/JobLogService.java new file mode 100644 index 0000000..3240ec0 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/JobLogService.java @@ -0,0 +1,61 @@ +package top.wms.admin.schedule.service; + +import top.wms.admin.schedule.model.JobInstanceLogPageResult; +import top.wms.admin.schedule.model.query.JobInstanceLogQuery; +import top.wms.admin.schedule.model.query.JobInstanceQuery; +import top.wms.admin.schedule.model.query.JobLogQuery; +import top.wms.admin.schedule.model.resp.JobInstanceResp; +import top.wms.admin.schedule.model.resp.JobLogResp; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; + +/** + * 任务日志业务接口 + * + * @author KAI + * @author Charles7c + * @since 2024/6/27 22:52 + */ +public interface JobLogService { + + /** + * 分页查询列表 + * + * @param query 查询条件 + * @return 分页列表信息 + */ + PageResp page(JobLogQuery query); + + /** + * 停止 + * + * @param id ID + * @return 停止结果 + */ + boolean stop(Long id); + + /** + * 重试 + * + * @param id ID + * @return 重试结果 + */ + boolean retry(Long id); + + /** + * 查询任务实例列表 + * + * @param query 查询条件 + * @return 列表信息 + */ + List listInstance(JobInstanceQuery query); + + /** + * 分页查询任务实例日志列表 + * + * @param query 查询条件 + * @return 分页列表信息 + */ + JobInstanceLogPageResult pageInstanceLog(JobInstanceLogQuery query); +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/JobService.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/JobService.java new file mode 100644 index 0000000..105e5a7 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/JobService.java @@ -0,0 +1,76 @@ +package top.wms.admin.schedule.service; + +import top.wms.admin.schedule.model.query.JobQuery; +import top.wms.admin.schedule.model.req.JobReq; +import top.wms.admin.schedule.model.req.JobStatusReq; +import top.wms.admin.schedule.model.resp.JobResp; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; + +/** + * 任务业务接口 + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 17:20 + */ +public interface JobService { + + /** + * 分页查询列表 + * + * @param query 查询条件 + * @return 分页列表信息 + */ + PageResp page(JobQuery query); + + /** + * 新增 + * + * @param req 创建信息 + * @return 新增结果 + */ + boolean add(JobReq req); + + /** + * 修改 + * + * @param req 修改信息 + * @param id ID + * @return 修改结果 + */ + boolean update(JobReq req, Long id); + + /** + * 修改状态 + * + * @param req 修改状态信息 + * @param id ID + * @return 修改状态结果 + */ + boolean updateStatus(JobStatusReq req, Long id); + + /** + * 删除 + * + * @param id ID + * @return 删除结果 + */ + boolean delete(Long id); + + /** + * 执行 + * + * @param id ID + * @return 执行结果 + */ + boolean trigger(Long id); + + /** + * 查询分组列表 + * + * @return 分组列表 + */ + List listGroup(); +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/impl/JobLogServiceImpl.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/impl/JobLogServiceImpl.java new file mode 100644 index 0000000..f5840e8 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/impl/JobLogServiceImpl.java @@ -0,0 +1,67 @@ +package top.wms.admin.schedule.service.impl; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.schedule.api.JobBatchApi; +import top.wms.admin.schedule.api.JobClient; +import top.wms.admin.schedule.model.JobInstanceLogPageResult; +import top.wms.admin.schedule.model.query.JobInstanceLogQuery; +import top.wms.admin.schedule.model.query.JobInstanceQuery; +import top.wms.admin.schedule.model.query.JobLogQuery; +import top.wms.admin.schedule.model.resp.JobInstanceResp; +import top.wms.admin.schedule.model.resp.JobLogResp; +import top.wms.admin.schedule.service.JobLogService; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +/** + * 任务日志业务实现 + * + * @author KAI + * @author Charles7c + * @since 2024/6/27 22:54 + */ +@Service +@RequiredArgsConstructor +public class JobLogServiceImpl implements JobLogService { + + private final JobClient jobClient; + private final JobBatchApi jobBatchApi; + + @Override + public PageResp page(JobLogQuery query) { + LocalDateTime[] datetimeRange = query.getDatetimeRange(); + return jobClient.requestPage(() -> jobBatchApi.page(query.getJobId(), query.getJobName(), query + .getGroupName(), query.getTaskBatchStatus() != null + ? query.getTaskBatchStatus().getValue() + : null, new String[] {DateUtil.format(datetimeRange[0], DatePattern.UTC_SIMPLE_PATTERN), DateUtil + .format(datetimeRange[1], DatePattern.UTC_SIMPLE_PATTERN)}, query.getPage(), query.getSize())); + } + + @Override + public boolean stop(Long id) { + return Boolean.TRUE.equals(jobClient.request(() -> jobBatchApi.stop(id))); + } + + @Override + public boolean retry(Long id) { + return Boolean.TRUE.equals(jobClient.request(() -> jobBatchApi.retry(id))); + } + + @Override + public List listInstance(JobInstanceQuery query) { + return jobClient.requestPage(() -> jobBatchApi.pageTask(query.getJobId(), query.getTaskBatchId(), 1, 100)) + .getList(); + } + + @Override + public JobInstanceLogPageResult pageInstanceLog(JobInstanceLogQuery query) { + return Objects.requireNonNull(jobBatchApi.pageLog(query.getJobId(), query.getTaskBatchId(), query + .getTaskId(), query.getStartId(), query.getFromIndex(), query.getSize()).getBody()).getData(); + } +} diff --git a/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/impl/JobServiceImpl.java b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/impl/JobServiceImpl.java new file mode 100644 index 0000000..c9fdc48 --- /dev/null +++ b/mes-plugin/mes-plugin-schedule/src/main/java/top/mes/admin/schedule/service/impl/JobServiceImpl.java @@ -0,0 +1,68 @@ +package top.wms.admin.schedule.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import top.wms.admin.schedule.api.JobApi; +import top.wms.admin.schedule.api.JobClient; +import top.wms.admin.schedule.model.query.JobQuery; +import top.wms.admin.schedule.model.req.JobReq; +import top.wms.admin.schedule.model.req.JobStatusReq; +import top.wms.admin.schedule.model.resp.JobResp; +import top.wms.admin.schedule.service.JobService; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.Collections; +import java.util.List; + +/** + * 任务业务实现 + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 17:25 + */ +@Service +@RequiredArgsConstructor +public class JobServiceImpl implements JobService { + + private final JobClient jobClient; + private final JobApi jobApi; + + @Override + public PageResp page(JobQuery query) { + return jobClient.requestPage(() -> jobApi.page(query.getGroupName(), query.getJobName(), query + .getJobStatus() != null ? query.getJobStatus().getValue() : null, query.getPage(), query.getSize())); + } + + @Override + public boolean add(JobReq req) { + return Boolean.TRUE.equals(jobClient.request(() -> jobApi.add(req))); + } + + @Override + public boolean update(JobReq req, Long id) { + req.setId(id); + return Boolean.TRUE.equals(jobClient.request(() -> jobApi.update(req))); + } + + @Override + public boolean updateStatus(JobStatusReq req, Long id) { + req.setId(id); + return Boolean.TRUE.equals(jobClient.request(() -> jobApi.updateStatus(req))); + } + + @Override + public boolean delete(Long id) { + return Boolean.TRUE.equals(jobClient.request(() -> jobApi.delete(Collections.singleton(id)))); + } + + @Override + public boolean trigger(Long id) { + return Boolean.TRUE.equals(jobClient.request(() -> jobApi.trigger(id))); + } + + @Override + public List listGroup() { + return jobClient.request(jobApi::listGroup); + } +} \ No newline at end of file diff --git a/mes-plugin/pom.xml b/mes-plugin/pom.xml new file mode 100644 index 0000000..71bd423 --- /dev/null +++ b/mes-plugin/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + top.wms + wms-admin + ${revision} + + + wms-plugin + pom + 插件模块(存放代码生成、任务调度等扩展模块) + + + + + top.wms + wms-common + + + \ No newline at end of file diff --git a/mes-webapi/pom.xml b/mes-webapi/pom.xml new file mode 100644 index 0000000..aee3a84 --- /dev/null +++ b/mes-webapi/pom.xml @@ -0,0 +1,240 @@ + + + 4.0.0 + + top.wms + wms-admin + ${revision} + + + wms-webapi + API 及打包部署模块 + + + + + top.wms.admin.WmsAdminApplication + + bin/ + + config/ + + lib/ + + + + + + top.continew + continew-starter-log-interceptor + + + com.yomahub + tlog-core + + + com.yomahub + tlog-webflux + + + com.yomahub + tlog-feign + + + com.yomahub + tlog-okhttp + + + com.yomahub + tlog-resttemplate + + + com.yomahub + tlog-xxljob + + + + + + + top.wms + wms-module-system + + + + + top.wms + wms-plugin-schedule + + + + + top.wms + wms-plugin-generator + + + + + org.liquibase + liquibase-core + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.amazonaws + aws-java-sdk-s3 + 1.12.780 + + + + + jakarta.websocket + jakarta.websocket-api + 2.1.0 + + + + + com.hikvision + MvCameraControlWrapper + 1.0 + system + ${project.basedir}/Library/MvCameraControlWrapper.jar + + + + + com.sun.jna + jna + 1.0 + system + ${project.basedir}/src/main/resources/lib/jna.jar + + + + + com.fazecast + jSerialComm + 2.10.5 + + + + + + + ${project.parent.name} + + + + org.apache.maven.plugins + maven-jar-plugin + + + + ${config-path} + db/ + templates/ + logback-spring.xml + + + + ${main-class} + + ../${lib-path} + true + + false + + + + ../${config-path} ../${config-path}lib/jna.jar ../${lib-path}jna.jar + + + ${project.build.directory}/app/${bin-path} + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/app/${lib-path} + runtime + + + + copy-system-dependencies + package + + copy-dependencies + + + ${project.build.directory}/app/${lib-path} + system + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-resources + package + + copy-resources + + + + + src/main/resources/${config-path} + + + src/main/resources + + db/ + templates/ + logback-spring.xml + + + + ${project.build.directory}/app/${config-path} + + + + copy-lib-resources + package + + copy-resources + + + + + src/main/resources/lib + + + ${project.build.directory}/app/${config-path}lib + + + + + + + \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/WmsAdminApplication.java b/mes-webapi/src/main/java/top/mes/admin/WmsAdminApplication.java new file mode 100644 index 0000000..be304a8 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/WmsAdminApplication.java @@ -0,0 +1,49 @@ +package top.wms.admin; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.alicp.jetcache.anno.config.EnableMethodCache; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.x.file.storage.spring.EnableFileStorage; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import top.continew.starter.core.autoconfigure.project.ProjectProperties; +import top.continew.starter.extension.crud.annotation.EnableCrudRestController; +import top.continew.starter.web.annotation.EnableGlobalResponse; +import top.continew.starter.web.model.R; + +/** + * 启动程序 + * + * @author Charles7c + * @since 2022/12/8 23:15 + */ +@Slf4j +@EnableFileStorage +@EnableMethodCache(basePackages = "top.wms.admin") +@EnableGlobalResponse +@EnableCrudRestController +@RestController +@SpringBootApplication +@RequiredArgsConstructor +@EnableScheduling +public class WmsAdminApplication { + + private final ProjectProperties projectProperties; + + public static void main(String[] args) { + SpringApplication.run(WmsAdminApplication.class, args); + } + + @Hidden + @SaIgnore + @GetMapping("/") + public R index() { + return R.ok(projectProperties); + } + +} diff --git a/mes-webapi/src/main/java/top/mes/admin/config/log/LogConfiguration.java b/mes-webapi/src/main/java/top/mes/admin/config/log/LogConfiguration.java new file mode 100644 index 0000000..d26a683 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/log/LogConfiguration.java @@ -0,0 +1,27 @@ +package top.wms.admin.config.log; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import top.wms.admin.system.mapper.LogMapper; +import top.wms.admin.system.service.UserService; +import top.continew.starter.log.annotation.ConditionalOnEnabledLog; +import top.continew.starter.log.dao.LogDao; + +/** + * 日志配置 + * + * @author Charles7c + * @since 2022/12/24 23:15 + */ +@Configuration +@ConditionalOnEnabledLog +public class LogConfiguration { + + /** + * 日志持久层接口本地实现类 + */ + @Bean + public LogDao logDao(UserService userService, LogMapper logMapper) { + return new LogDaoLocalImpl(userService, logMapper); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/config/log/LogDaoLocalImpl.java b/mes-webapi/src/main/java/top/mes/admin/config/log/LogDaoLocalImpl.java new file mode 100644 index 0000000..e0d9ccf --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/log/LogDaoLocalImpl.java @@ -0,0 +1,161 @@ +package top.wms.admin.config.log; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.HttpStatus; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.scheduling.annotation.Async; +import top.wms.admin.auth.enums.AuthTypeEnum; +import top.wms.admin.auth.model.req.*; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.system.enums.LogStatusEnum; +import top.wms.admin.system.mapper.LogMapper; +import top.wms.admin.system.model.entity.LogDO; +import top.wms.admin.system.service.UserService; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.ExceptionUtils; +import top.continew.starter.core.util.StrUtils; +import top.continew.starter.log.dao.LogDao; +import top.continew.starter.log.model.LogRecord; +import top.continew.starter.log.model.LogRequest; +import top.continew.starter.log.model.LogResponse; +import top.continew.starter.web.model.R; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; +import java.util.Set; + +/** + * 日志持久层接口本地实现类 + * + * @author Charles7c + * @since 2023/12/16 23:55 + */ +@RequiredArgsConstructor +public class LogDaoLocalImpl implements LogDao { + + private final UserService userService; + private final LogMapper logMapper; + + @Async + @Override + public void add(LogRecord logRecord) { + LogDO logDO = new LogDO(); + // 设置请求信息 + LogRequest logRequest = logRecord.getRequest(); + this.setRequest(logDO, logRequest); + // 设置响应信息 + LogResponse logResponse = logRecord.getResponse(); + this.setResponse(logDO, logResponse); + // 设置基本信息 + logDO.setDescription(logRecord.getDescription()); + logDO.setModule(StrUtils.blankToDefault(logRecord.getModule(), null, m -> m + .replace("API", StringConstants.EMPTY) + .trim())); + logDO.setTimeTaken(logRecord.getTimeTaken().toMillis()); + logDO.setCreateTime(LocalDateTime.ofInstant(logRecord.getTimestamp(), ZoneId.systemDefault())); + // 设置操作人 + this.setCreateUser(logDO, logRequest, logResponse); + logMapper.insert(logDO); + } + + /** + * 设置请求信息 + * + * @param logDO 日志信息 + * @param logRequest 请求信息 + */ + private void setRequest(LogDO logDO, LogRequest logRequest) { + logDO.setRequestMethod(logRequest.getMethod()); + logDO.setRequestUrl(logRequest.getUrl().toString()); + logDO.setRequestHeaders(JSONUtil.toJsonStr(logRequest.getHeaders())); + logDO.setRequestBody(logRequest.getBody()); + logDO.setIp(logRequest.getIp()); + logDO.setAddress(logRequest.getAddress()); + logDO.setBrowser(logRequest.getBrowser()); + logDO.setOs(StrUtil.subBefore(logRequest.getOs(), " or", false)); + } + + /** + * 设置响应信息 + * + * @param logDO 日志信息 + * @param logResponse 响应信息 + */ + private void setResponse(LogDO logDO, LogResponse logResponse) { + Map responseHeaders = logResponse.getHeaders(); + logDO.setResponseHeaders(JSONUtil.toJsonStr(responseHeaders)); + logDO.setTraceId(responseHeaders.get("traceId")); + String responseBody = logResponse.getBody(); + logDO.setResponseBody(responseBody); + // 状态 + Integer statusCode = logResponse.getStatus(); + logDO.setStatusCode(statusCode); + logDO.setStatus(statusCode >= HttpStatus.HTTP_BAD_REQUEST ? LogStatusEnum.FAILURE : LogStatusEnum.SUCCESS); + if (StrUtil.isNotBlank(responseBody)) { + R result = JSONUtil.toBean(responseBody, R.class); + if (!result.isSuccess()) { + logDO.setStatus(LogStatusEnum.FAILURE); + logDO.setErrorMsg(result.getMsg()); + } + } + } + + /** + * 设置操作人 + * + * @param logDO 日志信息 + * @param logRequest 请求信息 + * @param logResponse 响应信息 + */ + private void setCreateUser(LogDO logDO, LogRequest logRequest, LogResponse logResponse) { + String requestUri = URLUtil.getPath(logDO.getRequestUrl()); + // 解析退出接口信息 + String responseBody = logResponse.getBody(); + if (requestUri.startsWith(SysConstants.LOGOUT_URI) && StrUtil.isNotBlank(responseBody)) { + R result = JSONUtil.toBean(responseBody, R.class); + logDO.setCreateUser(Convert.toLong(result.getData(), null)); + return; + } + // 解析登录接口信息 + if (requestUri.startsWith(SysConstants.LOGIN_URI) && LogStatusEnum.SUCCESS.equals(logDO.getStatus())) { + String requestBody = logRequest.getBody(); + logDO.setDescription(JSONUtil.toBean(requestBody, LoginReq.class).getAuthType().getDescription() + "登录"); + // 解析账号登录用户为操作人 + if (requestBody.contains(AuthTypeEnum.ACCOUNT.getValue())) { + AccountLoginReq authReq = JSONUtil.toBean(requestBody, AccountLoginReq.class); + logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByUsername(authReq.getUsername()) + .getId())); + return; + } else if (requestBody.contains(AuthTypeEnum.EMAIL.getValue())) { + EmailLoginReq authReq = JSONUtil.toBean(requestBody, EmailLoginReq.class); + logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByEmail(authReq.getEmail()).getId())); + return; + } else if (requestBody.contains(AuthTypeEnum.PHONE.getValue())) { + PhoneLoginReq authReq = JSONUtil.toBean(requestBody, PhoneLoginReq.class); + logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByPhone(authReq.getPhone()).getId())); + return; + } + } + // 解析 Token 信息 + Map requestHeaders = logRequest.getHeaders(); + String headerName = HttpHeaders.AUTHORIZATION; + boolean isContainsAuthHeader = CollUtil.containsAny(requestHeaders.keySet(), Set.of(headerName, headerName + .toLowerCase())); + if (MapUtil.isNotEmpty(requestHeaders) && isContainsAuthHeader) { + String authorization = requestHeaders.getOrDefault(headerName, requestHeaders.get(headerName + .toLowerCase())); + String token = authorization.replace(SaManager.getConfig() + .getTokenPrefix() + StringConstants.SPACE, StringConstants.EMPTY); + logDO.setCreateUser(Convert.toLong(StpUtil.getLoginIdByToken(token))); + } + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/config/satoken/LoginPasswordProperties.java b/mes-webapi/src/main/java/top/mes/admin/config/satoken/LoginPasswordProperties.java new file mode 100644 index 0000000..0b627e8 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/satoken/LoginPasswordProperties.java @@ -0,0 +1,22 @@ +package top.wms.admin.config.satoken; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 密码配置属性 + * + * @author Charles7c + * @since 2024/6/15 22:15 + */ +@Data +@Component +@ConfigurationProperties(prefix = "auth.password") +public class LoginPasswordProperties { + + /** + * 排除(放行)路径配置 + */ + private String[] excludes = new String[0]; +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaExtensionInterceptor.java b/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaExtensionInterceptor.java new file mode 100644 index 0000000..d014464 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaExtensionInterceptor.java @@ -0,0 +1,46 @@ +package top.wms.admin.config.satoken; + +import cn.dev33.satoken.fun.SaParamFunction; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.stp.StpUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.lang.Nullable; +import top.wms.admin.common.context.UserContextHolder; + +/** + * Sa-Token 扩展拦截器 + * + * @author Charles7c + * @since 2024/10/10 20:25 + */ +public class SaExtensionInterceptor extends SaInterceptor { + + public SaExtensionInterceptor(SaParamFunction auth) { + super(auth); + } + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) throws Exception { + boolean flag = super.preHandle(request, response, handler); + if (flag && StpUtil.isLogin()) { + UserContextHolder.getContext(); + UserContextHolder.getExtraContext(); + } + return flag; + } + + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, + @Nullable Exception e) throws Exception { + try { + super.afterCompletion(request, response, handler, e); + } finally { + UserContextHolder.clearContext(); + } + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaTokenConfiguration.java b/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaTokenConfiguration.java new file mode 100644 index 0000000..c3c01dc --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaTokenConfiguration.java @@ -0,0 +1,73 @@ +package top.wms.admin.config.satoken; + +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.sign.SaSignTemplate; +import cn.dev33.satoken.sign.SaSignUtil; +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.stp.StpUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; +import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.exception.BusinessException; +import top.continew.starter.core.validation.CheckUtils; + +import java.util.List; + +/** + * Sa-Token 配置 + * + * @author Charles7c + * @author chengzi + * @since 2022/12/19 22:13 + */ +@Configuration +@RequiredArgsConstructor +public class SaTokenConfiguration { + + private final SaTokenExtensionProperties properties; + private final LoginPasswordProperties loginPasswordProperties; + + /** + * Sa-Token 权限认证配置 + */ + @Bean + public StpInterface stpInterface() { + return new SaTokenPermissionImpl(); + } + + /** + * SaToken 拦截器配置 + */ + @Bean + public SaInterceptor saInterceptor() { + return new SaExtensionInterceptor(handle -> SaRouter.match(StringConstants.PATH_PATTERN) + .notMatch(properties.getSecurity().getExcludes()) + .check(r -> { + // 如果包含 sign,进行 API 接口参数签名验证 + SaRequest saRequest = SaHolder.getRequest(); + List paramNames = saRequest.getParamNames(); + if (paramNames.stream().anyMatch(SaSignTemplate.sign::equals)) { + try { + SaSignUtil.checkRequest(saRequest); + } catch (Exception e) { + throw new BusinessException(e.getMessage()); + } + return; + } + // 不包含 sign 参数,进行普通登录验证 + StpUtil.checkLogin(); + if (SaRouter.isMatchCurrURI(loginPasswordProperties.getExcludes())) { + return; + } + UserContext userContext = UserContextHolder.getContext(); + CheckUtils.throwIf(userContext.isPasswordExpired(), "密码已过期,请修改密码"); + })); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaTokenPermissionImpl.java b/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaTokenPermissionImpl.java new file mode 100644 index 0000000..16d2704 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/satoken/SaTokenPermissionImpl.java @@ -0,0 +1,29 @@ +package top.wms.admin.config.satoken; + +import cn.dev33.satoken.stp.StpInterface; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sa-Token 权限认证实现 + * + * @author Charles7c + * @since 2023/3/1 22:28 + */ +public class SaTokenPermissionImpl implements StpInterface { + + @Override + public List getPermissionList(Object loginId, String loginType) { + UserContext userContext = UserContextHolder.getContext(); + return new ArrayList<>(userContext.getPermissions()); + } + + @Override + public List getRoleList(Object loginId, String loginType) { + UserContext userContext = UserContextHolder.getContext(); + return new ArrayList<>(userContext.getRoleCodes()); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/config/webSocket/WebSocketConfig.java b/mes-webapi/src/main/java/top/mes/admin/config/webSocket/WebSocketConfig.java new file mode 100644 index 0000000..805f118 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/config/webSocket/WebSocketConfig.java @@ -0,0 +1,18 @@ +package top.wms.admin.config.webSocket; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import top.wms.admin.controller.weighManage.ah.ScaleWebSocketHandler; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + // 注册WebSocket端点,允许所有跨域请求 + registry.addHandler(new ScaleWebSocketHandler(), "/ws/scale").setAllowedOrigins("*"); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/auth/AuthController.java b/mes-webapi/src/main/java/top/mes/admin/controller/auth/AuthController.java new file mode 100644 index 0000000..c48c90e --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/auth/AuthController.java @@ -0,0 +1,90 @@ +package top.wms.admin.controller.auth; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.auth.model.req.LoginReq; +import top.wms.admin.auth.model.resp.LoginResp; +import top.wms.admin.auth.model.resp.RouteResp; +import top.wms.admin.auth.model.resp.SocialAuthAuthorizeResp; +import top.wms.admin.auth.model.resp.UserInfoResp; +import top.wms.admin.auth.service.AuthService; +import top.wms.admin.common.context.UserContext; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.system.model.resp.user.UserDetailResp; +import top.wms.admin.system.service.UserService; +import top.continew.starter.core.exception.BadRequestException; +import top.continew.starter.log.annotation.Log; + +import java.util.List; + +/** + * 认证 API + * + * @author Charles7c + * @since 2022/12/21 20:37 + */ +@Tag(name = "认证 API") +@Log(module = "登录") +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/auth") +public class AuthController { + + private final AuthService authService; + private final UserService userService; + + @SaIgnore + @Operation(summary = "登录", description = "用户登录") + @PostMapping("/login") + public LoginResp login(@Validated @RequestBody LoginReq req, HttpServletRequest request) { + return authService.login(req, request); + } + + @Operation(summary = "登出", description = "注销用户的当前登录") + @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER) + @PostMapping("/logout") + public Object logout() { + Object loginId = StpUtil.getLoginId(-1L); + StpUtil.logout(); + return loginId; + } + + @SaIgnore + @Operation(summary = "三方账号登录授权", description = "三方账号登录授权") + @Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH) + @GetMapping("/{source}") + public SocialAuthAuthorizeResp authorize(@PathVariable String source) { + // 第三方登录已禁用 + throw new BadRequestException("第三方登录功能已禁用"); + } + + @Log(ignore = true) + @Operation(summary = "获取用户信息", description = "获取登录用户信息") + @GetMapping("/user/info") + public UserInfoResp getUserInfo() { + UserContext userContext = UserContextHolder.getContext(); + UserDetailResp userDetailResp = userService.get(userContext.getId()); + UserInfoResp userInfoResp = BeanUtil.copyProperties(userDetailResp, UserInfoResp.class); + userInfoResp.setPermissions(userContext.getPermissions()); + userInfoResp.setRoles(userContext.getRoleCodes()); + userInfoResp.setPwdExpired(userContext.isPasswordExpired()); + return userInfoResp; + } + + @Log(ignore = true) + @Operation(summary = "获取路由信息", description = "获取登录用户的路由信息") + @GetMapping("/user/route") + public List listRoute() { + return authService.buildRouteTree(UserContextHolder.getUserId()); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/code/GeneratorController.java b/mes-webapi/src/main/java/top/mes/admin/controller/code/GeneratorController.java new file mode 100644 index 0000000..d0a1434 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/code/GeneratorController.java @@ -0,0 +1,107 @@ +package top.wms.admin.controller.code; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.generator.model.entity.FieldConfigDO; +import top.wms.admin.generator.model.entity.GenConfigDO; +import top.wms.admin.generator.model.query.GenConfigQuery; +import top.wms.admin.generator.model.req.GenConfigReq; +import top.wms.admin.generator.model.resp.GeneratePreviewResp; +import top.wms.admin.generator.service.GeneratorService; +import top.wms.admin.system.service.DictService; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.sql.SQLException; +import java.util.List; + +/** + * 代码生成 API + * + * @author Charles7c + * @since 2023/8/3 22:58 + */ +@Tag(name = "代码生成 API") +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/code/generator") +public class GeneratorController { + + private final GeneratorService baseService; + private final DictService dictService; + + @Operation(summary = "分页查询生成配置", description = "分页查询生成配置列表") + @SaCheckPermission("code:generator:list") + @GetMapping("/config") + public PageResp pageGenConfig(GenConfigQuery query, @Validated PageQuery pageQuery) { + return baseService.pageGenConfig(query, pageQuery); + } + + @Operation(summary = "查询生成配置信息", description = "查询生成配置信息") + @Parameter(name = "tableName", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH) + @SaCheckPermission("code:generator:list") + @GetMapping("/config/{tableName}") + public GenConfigDO getGenConfig(@PathVariable String tableName) throws SQLException { + return baseService.getGenConfig(tableName); + } + + @Operation(summary = "查询字段配置列表", description = "查询字段配置列表") + @Parameter(name = "tableName", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH) + @Parameter(name = "requireSync", description = "是否需要同步", example = "false", in = ParameterIn.QUERY) + @SaCheckPermission("code:generator:config") + @GetMapping("/field/{tableName}") + public List listFieldConfig(@PathVariable String tableName, + @RequestParam(required = false, defaultValue = "false") Boolean requireSync) { + return baseService.listFieldConfig(tableName, requireSync); + } + + @Operation(summary = "保存配置信息", description = "保存配置信息") + @Parameter(name = "tableName", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH) + @SaCheckPermission("code:generator:config") + @PostMapping("/config/{tableName}") + public void saveConfig(@Validated @RequestBody GenConfigReq req, @PathVariable String tableName) { + baseService.saveConfig(req, tableName); + } + + @Operation(summary = "生成预览", description = "预览生成代码") + @Parameter(name = "tableNames", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH) + @SaCheckPermission("code:generator:preview") + @GetMapping("/preview/{tableNames}") + public List preview(@PathVariable List tableNames) { + return baseService.preview(tableNames); + } + + @Operation(summary = "生成下载代码", description = "生成下载代码") + @Parameter(name = "tableNames", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH) + @SaCheckPermission("code:generator:generate") + @PostMapping("/{tableNames}/download") + public void downloadCode(@PathVariable List tableNames, HttpServletResponse response) { + baseService.downloadCode(tableNames, response); + } + + @Operation(summary = "生成代码", description = "生成代码") + @Parameter(name = "tableNames", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH) + @SaCheckPermission("code:generator:generate") + @PostMapping("/{tableNames}") + public void generateCode(@PathVariable List tableNames) { + baseService.generateCode(tableNames); + } + + @Operation(summary = "查询字典", description = "查询字典列表") + @SaCheckPermission("code:generator:config") + @GetMapping("/dict") + public List listDict() { + List dictList = dictService.listDict(null, null); + dictList.addAll(dictService.listEnumDict()); + return dictList; + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/common/CaptchaController.java b/mes-webapi/src/main/java/top/mes/admin/controller/common/CaptchaController.java new file mode 100644 index 0000000..b41d79e --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/common/CaptchaController.java @@ -0,0 +1,121 @@ +package top.wms.admin.controller.common; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.RegexPool; +import cn.hutool.core.util.IdUtil; +import com.anji.captcha.model.common.RepCodeEnum; +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; +import com.wf.captcha.base.Captcha; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.auth.model.resp.CaptchaResp; +import top.wms.admin.common.config.properties.CaptchaProperties; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.constant.SysConstants; +import top.wms.admin.system.service.OptionService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.captcha.graphic.core.GraphicCaptchaService; +import top.continew.starter.core.autoconfigure.project.ProjectProperties; +import top.continew.starter.log.annotation.Log; +import top.continew.starter.web.model.R; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 验证码 API + * + * @author Charles7c + * @since 2022/12/11 14:00 + */ +@Tag(name = "验证码 API") +@SaIgnore +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/captcha") +public class CaptchaController { + + private final ProjectProperties projectProperties; + private final CaptchaProperties captchaProperties; + private final GraphicCaptchaService graphicCaptchaService; + private final OptionService optionService; + + @Log(ignore = true) + @Operation(summary = "获取行为验证码", description = "获取行为验证码(Base64编码)") + @GetMapping("/behavior") + public Object getBehaviorCaptcha(CaptchaVO captchaReq, HttpServletRequest request) { + // 行为验证码已禁用,返回默认成功响应 + Map result = new LinkedHashMap<>(); + result.put("captchaId", IdUtil.fastUUID()); + result.put("picPath", "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); + return result; + } + + @Log(ignore = true) + @Operation(summary = "校验行为验证码", description = "校验行为验证码") + @PostMapping("/behavior") + public Object checkBehaviorCaptcha(@RequestBody CaptchaVO captchaReq, HttpServletRequest request) { + // 行为验证码已禁用,直接返回成功 + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepCode(RepCodeEnum.SUCCESS.getCode()); + responseModel.setRepMsg("验证成功"); + return responseModel; + } + + @Log(ignore = true) + @Operation(summary = "获取图片验证码", description = "获取图片验证码(Base64编码,带图片格式:data:image/gif;base64)") + @GetMapping("/image") + public CaptchaResp getImageCaptcha() { + int loginCaptchaEnabled = optionService.getValueByCode2Int("LOGIN_CAPTCHA_ENABLED"); + if (SysConstants.NO.equals(loginCaptchaEnabled)) { + return CaptchaResp.builder().isEnabled(false).build(); + } + String uuid = IdUtil.fastUUID(); + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + uuid; + Captcha captcha = graphicCaptchaService.getCaptcha(); + long expireTime = LocalDateTimeUtil.toEpochMilli(LocalDateTime.now() + .plusMinutes(captchaProperties.getExpirationInMinutes())); + RedisUtils.set(captchaKey, captcha.text(), Duration.ofMinutes(captchaProperties.getExpirationInMinutes())); + return CaptchaResp.of(uuid, captcha.toBase64(), expireTime); + } + + /** + * 获取邮箱验证码(已禁用) + * + * @param email 邮箱 + * @return / + */ + @Operation(summary = "获取邮箱验证码", description = "发送验证码到指定邮箱") + @GetMapping("/mail") + public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") String email, + CaptchaVO captchaReq) { + // 邮箱验证码已禁用,直接返回成功 + return R.ok("发送成功,验证码有效期 5 分钟"); + } + + /** + * 获取短信验证码(已禁用) + * + * @param phone 手机号 + * @param captchaReq 行为验证码信息 + * @return / + */ + @Operation(summary = "获取短信验证码", description = "发送验证码到指定手机号") + @GetMapping("/sms") + public R getSmsCaptcha(@NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexPool.MOBILE, message = "手机号格式错误") String phone, + CaptchaVO captchaReq) { + // 短信验证码已禁用,直接返回成功 + return R.ok("发送成功,验证码有效期 5 分钟"); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/common/CommonController.java b/mes-webapi/src/main/java/top/mes/admin/controller/common/CommonController.java new file mode 100644 index 0000000..94dd76b --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/common/CommonController.java @@ -0,0 +1,158 @@ +package top.wms.admin.controller.common; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.lang.tree.Tree; +import cn.hutool.core.util.StrUtil; +import com.alicp.jetcache.anno.Cached; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.dromara.x.file.storage.core.FileInfo; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.util.PictureUtils; +import top.wms.admin.system.enums.OptionCategoryEnum; +import top.wms.admin.system.model.query.*; +import top.wms.admin.system.model.resp.file.FileUploadResp; +import top.wms.admin.system.service.*; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.extension.crud.model.query.SortQuery; +import top.continew.starter.extension.crud.model.resp.LabelValueResp; +import top.continew.starter.log.annotation.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * 公共 API + * + * @author Charles7c + * @since 2023/1/22 21:48 + */ +@Tag(name = "公共 API") +@Log(ignore = true) +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/common") +public class CommonController { + + private final FileService fileService; + private final DeptService deptService; + private final MenuService menuService; + private final UserService userService; + private final RoleService roleService; + private final DictItemService dictItemService; + private final OptionService optionService; + + @Operation(summary = "上传文件", description = "上传文件") + @PostMapping("/file") + public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file) { + ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); + FileInfo fileInfo = fileService.upload(file, true); + return FileUploadResp.builder() + .id(fileInfo.getId()) + .url(fileInfo.getUrl()) + .thUrl(fileInfo.getThUrl()) + .metadata(fileInfo.getMetadata()) + .build(); + } + + @Operation(summary = "上传文件", description = "上传文件") + @PostMapping("/updateUpload") + public FileUploadResp updateUpload(@NotNull(message = "文件不能为空") MultipartFile file, String savePath) { + ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); + FileInfo fileInfo = fileService.upload(file, savePath, null, false, false); + return FileUploadResp.builder() + .id(fileInfo.getId()) + .url(fileInfo.getUrl()) + .thUrl(fileInfo.getThUrl()) + .metadata(fileInfo.getMetadata()) + .build(); + } + + @Operation(summary = "上传文件", description = "上传文件") + @PostMapping("/faceUpload") + public FileUploadResp faceUpload(@NotNull(message = "图片不能为空") MultipartFile file, String savePath) { + ValidationUtils.throwIf(file::isEmpty, "图片不能为空"); + + // 判断是否需要压缩图片 + if ((1024 * 1024 * 0.1) <= file.getSize()) { + try (InputStream inputStream = file.getInputStream()) { + float quality; + // 根据文件大小设置不同的压缩质量 + if ((1024 * 1024 * 0.1) <= file.getSize() && file.getSize() <= (1024 * 1024)) { + quality = 0.4f; // 小于1M的图片 + } else if ((1024 * 1024) < file.getSize() && file.getSize() <= (1024 * 1024 * 2)) { + quality = 0.2f; // 1-2M的图片 + } else { + quality = 0.1f; // 2M以上的图片 + } + + // 直接在内存中压缩图片,避免使用临时文件 + file = PictureUtils.compressImage(inputStream, file.getOriginalFilename(), quality); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + FileInfo fileInfo = fileService.upload(file, savePath, null, false, false); + return FileUploadResp.builder() + .id(fileInfo.getId()) + .url(fileInfo.getUrl()) + .thUrl(fileInfo.getThUrl()) + .metadata(fileInfo.getMetadata()) + .build(); + } + + @Operation(summary = "查询部门树", description = "查询树结构的部门列表") + @GetMapping("/tree/dept") + public List> listDeptTree(DeptQuery query, SortQuery sortQuery) { + return deptService.tree(query, sortQuery, true); + } + + @Operation(summary = "查询菜单树", description = "查询树结构的菜单列表") + @GetMapping("/tree/menu") + public List> listMenuTree(MenuQuery query, SortQuery sortQuery) { + return menuService.tree(query, sortQuery, true); + } + + @Operation(summary = "查询用户字典", description = "查询用户字典列表") + @GetMapping("/dict/user") + public List listUserDict(UserQuery query, SortQuery sortQuery) { + return userService.listDict(query, sortQuery); + } + + @Operation(summary = "查询角色字典", description = "查询角色字典列表") + @GetMapping("/dict/role") + public List listRoleDict(RoleQuery query, SortQuery sortQuery) { + return roleService.listDict(query, sortQuery); + } + + @Operation(summary = "查询字典", description = "查询字典列表") + @Parameter(name = "code", description = "字典编码", example = "notice_type", in = ParameterIn.PATH) + @GetMapping("/dict/{code}") + public List listDict(@PathVariable String code) { + return dictItemService.listByDictCode(code); + } + + @SaIgnore + @Operation(summary = "查询系统配置参数", description = "查询系统配置参数") + @GetMapping("/dict/option/site") + @Cached(key = "'SITE'", name = CacheConstants.OPTION_KEY_PREFIX) + public List> listSiteOptionDict() { + OptionQuery optionQuery = new OptionQuery(); + optionQuery.setCategory(OptionCategoryEnum.SITE.name()); + return optionService.list(optionQuery) + .stream() + .map(option -> new LabelValueResp<>(option.getCode(), StrUtil.nullToDefault(option.getValue(), option + .getDefaultValue()))) + .toList(); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/monitor/OnlineUserController.java b/mes-webapi/src/main/java/top/mes/admin/controller/monitor/OnlineUserController.java new file mode 100644 index 0000000..d95a290 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/monitor/OnlineUserController.java @@ -0,0 +1,49 @@ +package top.wms.admin.controller.monitor; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.stp.StpUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.auth.model.query.OnlineUserQuery; +import top.wms.admin.auth.model.resp.OnlineUserResp; +import top.wms.admin.auth.service.OnlineUserService; +import top.continew.starter.core.validation.CheckUtils; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +/** + * 在线用户 API + * + * @author Charles7c + * @since 2023/1/20 21:51 + */ +@Tag(name = "在线用户 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/monitor/online") +public class OnlineUserController { + + private final OnlineUserService baseService; + + @Operation(summary = "分页查询列表", description = "分页查询列表") + @SaCheckPermission("monitor:online:list") + @GetMapping + public PageResp page(OnlineUserQuery query, @Validated PageQuery pageQuery) { + return baseService.page(query, pageQuery); + } + + @Operation(summary = "强退在线用户", description = "强退在线用户") + @Parameter(name = "token", description = "令牌", example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiTUd6djdyOVFoeHEwdVFqdFAzV3M5YjVJRzh4YjZPSEUifQ.7q7U3ouoN7WPhH2kUEM7vPe5KF3G_qavSG-vRgIxKvE", in = ParameterIn.PATH) + @SaCheckPermission("monitor:online:kickout") + @DeleteMapping("/{token}") + public void kickout(@PathVariable String token) { + String currentToken = StpUtil.getTokenValue(); + CheckUtils.throwIfEqual(token, currentToken, "不能强退自己"); + StpUtil.kickoutByTokenValue(token); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/schedule/DemoEnvironmentJob.java b/mes-webapi/src/main/java/top/mes/admin/controller/schedule/DemoEnvironmentJob.java new file mode 100644 index 0000000..1ec95cf --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/schedule/DemoEnvironmentJob.java @@ -0,0 +1,139 @@ +package top.wms.admin.controller.schedule; + +import cn.hutool.core.util.StrUtil; +import com.aizuda.snailjob.client.job.core.annotation.JobExecutor; +import com.aizuda.snailjob.common.log.SnailJobLog; +import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy; +import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.system.mapper.*; +import top.wms.admin.system.model.entity.*; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.constant.StringConstants; + +import java.util.List; +import java.util.function.BooleanSupplier; + +/** + * 演示环境任务(任务示例) + * + * @author Charles7c + * @since 2024/8/4 15:30 + */ +@Component +@RequiredArgsConstructor +public class DemoEnvironmentJob { + + private final DictItemMapper dictItemMapper; + private final DictMapper dictMapper; + private final StorageMapper storageMapper; + private final UserMapper userMapper; + private final UserRoleMapper userRoleMapper; + private final UserSocialMapper userSocialMapper; + private final RoleMapper roleMapper; + private final RoleDeptMapper roleDeptMapper; + private final RoleMenuMapper roleMenuMapper; + private final MenuMapper menuMapper; + private final DeptMapper deptMapper; + + private final ClientMapper clientsMapper; + + private static final Long DELETE_FLAG = 10000L; + private static final Long MESSAGE_FLAG = 0L; + private static final List USER_FLAG = List + .of(1L, 547889293968801822L, 547889293968801823L, 547889293968801824L, 547889293968801825L, 547889293968801826L, 547889293968801827L, 547889293968801828L, 547889293968801829L, 547889293968801830L, 547889293968801831L); + private static final List ROLE_FLAG = List.of(1L, 547888897925840927L, 547888897925840928L); + private static final Long DEPT_FLAG = 547887852587843611L; + + /** + * 重置演示环境数据 + */ + @JobExecutor(name = "ResetEnvironmentData") + @Transactional(rollbackFor = Exception.class) + public void resetEnvironmentData() { + try { + SnailJobLog.REMOTE.info("定时任务 [重置演示环境数据] 开始执行。"); + // 检测待清理数据 + SnailJobLog.REMOTE.info("开始检测演示环境待清理数据项,请稍候..."); + Long dictItemCount = dictItemMapper.lambdaQuery().gt(DictItemDO::getId, DELETE_FLAG).count(); + this.log(dictItemCount, "字典项"); + Long dictCount = dictMapper.lambdaQuery().gt(DictDO::getId, DELETE_FLAG).count(); + this.log(dictCount, "字典"); + Long storageCount = storageMapper.lambdaQuery().gt(StorageDO::getId, DELETE_FLAG).count(); + this.log(storageCount, "存储"); + Long userCount = userMapper.lambdaQuery().notIn(UserDO::getId, USER_FLAG).count(); + this.log(userCount, "用户"); + Long roleCount = roleMapper.lambdaQuery().notIn(RoleDO::getId, ROLE_FLAG).count(); + this.log(roleCount, "角色"); + Long menuCount = menuMapper.lambdaQuery().gt(MenuDO::getId, DELETE_FLAG).count(); + this.log(menuCount, "菜单"); + Long deptCount = deptMapper.lambdaQuery().gt(DeptDO::getId, DEPT_FLAG).count(); + this.log(deptCount, "部门"); + Long clientCount = clientsMapper.lambdaQuery().gt(ClientDO::getId, DELETE_FLAG).count(); + this.log(clientCount, "终端"); + InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().blockAttack(true).build()); + SnailJobLog.REMOTE.info("演示环境待清理数据项检测完成,开始执行清理。"); + userRoleMapper.lambdaUpdate().notIn(UserRoleDO::getRoleId, ROLE_FLAG).remove(); + userRoleMapper.lambdaUpdate().notIn(UserRoleDO::getUserId, USER_FLAG).remove(); + roleDeptMapper.lambdaUpdate().notIn(RoleDeptDO::getRoleId, ROLE_FLAG).remove(); + roleMenuMapper.lambdaUpdate().notIn(RoleMenuDO::getRoleId, ROLE_FLAG).remove(); + userSocialMapper.lambdaUpdate().notIn(UserSocialDO::getUserId, USER_FLAG).remove(); + // 清理具体数据 + this.clean(dictItemCount, "字典项", null, () -> dictItemMapper.lambdaUpdate() + .gt(DictItemDO::getId, DELETE_FLAG) + .remove()); + this.clean(dictCount, "字典", CacheConstants.DICT_KEY_PREFIX, () -> dictMapper.lambdaUpdate() + .gt(DictDO::getId, DELETE_FLAG) + .remove()); + this.clean(storageCount, "存储", null, () -> storageMapper.lambdaUpdate() + .gt(StorageDO::getId, DELETE_FLAG) + .remove()); + this.clean(userCount, "用户", null, () -> userMapper.lambdaUpdate().notIn(UserDO::getId, USER_FLAG).remove()); + this.clean(roleCount, "角色", null, () -> roleMapper.lambdaUpdate().notIn(RoleDO::getId, ROLE_FLAG).remove()); + this.clean(menuCount, "菜单", CacheConstants.ROLE_MENU_KEY_PREFIX, () -> menuMapper.lambdaUpdate() + .gt(MenuDO::getId, DELETE_FLAG) + .remove()); + this.clean(deptCount, "部门", null, () -> deptMapper.lambdaUpdate().gt(DeptDO::getId, DEPT_FLAG).remove()); + this.clean(clientCount, "终端", null, () -> clientsMapper.lambdaUpdate() + .gt(ClientDO::getId, DEPT_FLAG) + .remove()); + SnailJobLog.REMOTE.info("演示环境数据已清理完成。"); + SnailJobLog.REMOTE.info("定时任务 [重置演示环境数据] 执行结束。"); + } finally { + InterceptorIgnoreHelper.clearIgnoreStrategy(); + } + } + + /** + * 输出日志 + * + * @param count 待清理数据项数量 + * @param resource 资源名称 + */ + private void log(Long count, String resource) { + if (count > 0) { + SnailJobLog.REMOTE.info("检测到 [{}] 待清理数据项:{}条", resource, count); + } + } + + /** + * 清理数据 + * + * @param count 待清理数据项数量 + * @param resource 资源名称 + * @param cacheKey 缓存键 + * @param supplier 清理数据项函数 + */ + private void clean(Long count, String resource, String cacheKey, BooleanSupplier supplier) { + if (count > 0 && supplier.getAsBoolean()) { + SnailJobLog.REMOTE.info("[{}] 数据项清理完成。", resource); + if (StrUtil.isNotBlank(cacheKey)) { + RedisUtils.deleteByPattern(cacheKey + StringConstants.ASTERISK); + SnailJobLog.REMOTE.info("[{}] 数据项缓存清理完成。", resource); + } + } + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/schedule/JobController.java b/mes-webapi/src/main/java/top/mes/admin/controller/schedule/JobController.java new file mode 100644 index 0000000..a51b083 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/schedule/JobController.java @@ -0,0 +1,90 @@ +package top.wms.admin.controller.schedule; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.schedule.model.query.JobQuery; +import top.wms.admin.schedule.model.req.JobReq; +import top.wms.admin.schedule.model.req.JobStatusReq; +import top.wms.admin.schedule.model.resp.JobResp; +import top.wms.admin.schedule.service.JobService; +import top.continew.starter.extension.crud.model.resp.PageResp; +import top.continew.starter.extension.crud.validation.CrudValidationGroup; +import top.continew.starter.log.annotation.Log; + +import java.util.List; + +/** + * 任务 API + * + * @author KAI + * @author Charles7c + * @since 2024/6/25 22:24 + */ +@Tag(name = " 任务 API") +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/schedule/job") +public class JobController { + + private final JobService baseService; + + @Operation(summary = "分页查询任务列表", description = "分页查询任务列表") + @SaCheckPermission("schedule:job:list") + @GetMapping + public PageResp page(JobQuery query) { + return baseService.page(query); + } + + @Operation(summary = "新增任务", description = "新增任务") + @SaCheckPermission("schedule:job:add") + @PostMapping + public void add(@Validated(CrudValidationGroup.Add.class) @RequestBody JobReq req) { + baseService.add(req); + } + + @Operation(summary = "修改任务", description = "修改任务") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("schedule:job:update") + @PutMapping("/{id}") + public void update(@Validated(CrudValidationGroup.Update.class) @RequestBody JobReq req, @PathVariable Long id) { + baseService.update(req, id); + } + + @Operation(summary = "修改任务状态", description = "修改任务状态") + @SaCheckPermission("schedule:job:update") + @PatchMapping("/{id}/status") + public void updateStatus(@Validated @RequestBody JobStatusReq req, @PathVariable Long id) { + baseService.updateStatus(req, id); + } + + @Operation(summary = "删除任务", description = "删除任务") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("schedule:job:delete") + @DeleteMapping("/{id}") + public void delete(@PathVariable Long id) { + baseService.delete(id); + } + + @Operation(summary = "执行任务", description = "执行任务") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("schedule:job:trigger") + @PostMapping("/trigger/{id}") + public void trigger(@PathVariable Long id) { + baseService.trigger(id); + } + + @Log(ignore = true) + @Operation(summary = "查询任务分组列表", description = "查询任务分组列表") + @SaCheckPermission("schedule:job:list") + @GetMapping("/group") + public List listGroup() { + return baseService.listGroup(); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/schedule/JobLogController.java b/mes-webapi/src/main/java/top/mes/admin/controller/schedule/JobLogController.java new file mode 100644 index 0000000..1d8842d --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/schedule/JobLogController.java @@ -0,0 +1,74 @@ +package top.wms.admin.controller.schedule; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.schedule.model.JobInstanceLogPageResult; +import top.wms.admin.schedule.model.query.JobInstanceLogQuery; +import top.wms.admin.schedule.model.query.JobInstanceQuery; +import top.wms.admin.schedule.model.query.JobLogQuery; +import top.wms.admin.schedule.model.resp.JobInstanceResp; +import top.wms.admin.schedule.model.resp.JobLogResp; +import top.wms.admin.schedule.service.JobLogService; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; + +/** + * 任务日志 API + * + * @author KAI + * @author Charles7c + * @since 2024/6/27 22:24 + */ +@Tag(name = " 任务日志 API") +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/schedule/log") +public class JobLogController { + + private final JobLogService baseService; + + @Operation(summary = "分页查询任务日志列表", description = "分页查询任务日志列表") + @SaCheckPermission("schedule:log:list") + @GetMapping + public PageResp page(JobLogQuery query) { + return baseService.page(query); + } + + @Operation(summary = "停止任务", description = "停止任务") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("schedule:log:stop") + @PostMapping("/stop/{id}") + public void stop(@PathVariable Long id) { + baseService.stop(id); + } + + @Operation(summary = "重试任务", description = "重试任务") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("schedule:log:retry") + @PostMapping("/retry/{id}") + public void retry(@PathVariable Long id) { + baseService.retry(id); + } + + @Operation(summary = "查询任务实例列表", description = "查询任务实例列表") + @SaCheckPermission("schedule:log:list") + @GetMapping("/instance") + public List listInstance(JobInstanceQuery query) { + return baseService.listInstance(query); + } + + @Operation(summary = "分页查询任务实例日志列表", description = "分页查询任务实例日志列表") + @SaCheckPermission("schedule:log:list") + @GetMapping("/instance/log") + public JobInstanceLogPageResult pageInstanceLog(JobInstanceLogQuery query) { + return baseService.pageInstanceLog(query); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/ClientController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/ClientController.java new file mode 100644 index 0000000..7ca4c4d --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/ClientController.java @@ -0,0 +1,23 @@ +package top.wms.admin.controller.system; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.ClientQuery; +import top.wms.admin.system.model.req.ClientReq; +import top.wms.admin.system.model.resp.ClientResp; +import top.wms.admin.system.service.ClientService; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; + +/** + * 终端管理 API + * + * @author KAI + * @since 2024/12/03 16:04 + */ +@Tag(name = "终端管理 API") +@RestController +@CrudRequestMapping(value = "/system/client", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) +public class ClientController extends BaseController { +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/ConfigController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/ConfigController.java new file mode 100644 index 0000000..841e074 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/ConfigController.java @@ -0,0 +1,27 @@ +package top.wms.admin.controller.system; + +import top.continew.starter.extension.crud.enums.Api; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.springframework.web.bind.annotation.*; + +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.ConfigQuery; +import top.wms.admin.system.model.req.ConfigReq; +import top.wms.admin.system.model.resp.ConfigResp; +import top.wms.admin.system.service.ConfigService; + +/** + * 参数配置管理 API + * + * @author zc + * @since 2025/12/30 12:44 + */ +@Tag(name = "参数配置管理 API") +@RestController +@CrudRequestMapping(value = "/system/config", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) +public class ConfigController extends BaseController { + +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/DeptController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/DeptController.java new file mode 100644 index 0000000..ae9e751 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/DeptController.java @@ -0,0 +1,23 @@ +package top.wms.admin.controller.system; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.DeptQuery; +import top.wms.admin.system.model.req.DeptReq; +import top.wms.admin.system.model.resp.DeptResp; +import top.wms.admin.system.service.DeptService; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; + +/** + * 部门管理 API + * + * @author Charles7c + * @since 2023/1/22 17:50 + */ +@Tag(name = "部门管理 API") +@RestController +@CrudRequestMapping(value = "/system/dept", api = {Api.TREE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) +public class DeptController extends BaseController { +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/DictController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/DictController.java new file mode 100644 index 0000000..6d75e45 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/DictController.java @@ -0,0 +1,36 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.DictQuery; +import top.wms.admin.system.model.req.DictReq; +import top.wms.admin.system.model.resp.DictResp; +import top.wms.admin.system.service.DictService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; + +/** + * 字典管理 API + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Tag(name = "字典管理 API") +@RestController +@CrudRequestMapping(value = "/system/dict", api = {Api.LIST, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) +public class DictController extends BaseController { + + @Operation(summary = "清除缓存", description = "清除缓存") + @SaCheckPermission("system:dict:clearCache") + @DeleteMapping("/cache/{code}") + public void clearCache(@PathVariable String code) { + RedisUtils.deleteByPattern(CacheConstants.DICT_KEY_PREFIX + code); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/DictItemController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/DictItemController.java new file mode 100644 index 0000000..4a1facf --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/DictItemController.java @@ -0,0 +1,25 @@ +package top.wms.admin.controller.system; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.DictItemQuery; +import top.wms.admin.system.model.req.DictItemReq; +import top.wms.admin.system.model.resp.DictItemResp; +import top.wms.admin.system.service.DictItemService; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; +import top.continew.starter.log.annotation.Log; + +/** + * 字典项管理 API + * + * @author Charles7c + * @since 2023/9/11 21:29 + */ +@Log(module = "字典管理") +@Tag(name = "字典项管理 API") +@RestController +@CrudRequestMapping(value = "/system/dict/item", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) +public class DictItemController extends BaseController { +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/FileController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/FileController.java new file mode 100644 index 0000000..882f237 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/FileController.java @@ -0,0 +1,38 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.FileQuery; +import top.wms.admin.system.model.req.FileReq; +import top.wms.admin.system.model.resp.file.FileResp; +import top.wms.admin.system.model.resp.file.FileStatisticsResp; +import top.wms.admin.system.service.FileService; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; +import top.continew.starter.log.annotation.Log; + +/** + * 文件管理 API + * + * @author Charles7c + * @since 2023/12/23 10:38 + */ +@Tag(name = "文件管理 API") +@RestController +@RequiredArgsConstructor +@CrudRequestMapping(value = "/system/file", api = {Api.PAGE, Api.UPDATE, Api.DELETE}) +public class FileController extends BaseController { + + @Log(ignore = true) + @Operation(summary = "查询文件资源统计", description = "查询文件资源统计") + @SaCheckPermission("system:file:list") + @GetMapping("/statistics") + public FileStatisticsResp statistics() { + return baseService.statistics(); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/LogController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/LogController.java new file mode 100644 index 0000000..27a237d --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/LogController.java @@ -0,0 +1,68 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.feiniaojin.gracefulresponse.api.ExcludeFromGracefulResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.system.model.query.LogQuery; +import top.wms.admin.system.model.resp.log.LogDetailResp; +import top.wms.admin.system.model.resp.log.LogResp; +import top.wms.admin.system.service.LogService; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.query.SortQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +/** + * 系统日志 API + * + * @author Charles7c + * @since 2023/1/18 23:55 + */ +@Tag(name = "系统日志 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/log") +public class LogController { + + private final LogService baseService; + + @Operation(summary = "分页查询列表", description = "分页查询列表") + @SaCheckPermission("monitor:log:list") + @GetMapping + public PageResp page(LogQuery query, @Validated PageQuery pageQuery) { + return baseService.page(query, pageQuery); + } + + @Operation(summary = "查询详情", description = "查询详情") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("monitor:log:detail") + @GetMapping("/{id}") + public LogDetailResp get(@PathVariable Long id) { + return baseService.get(id); + } + + @ExcludeFromGracefulResponse + @Operation(summary = "导出登录日志", description = "导出登录日志") + @SaCheckPermission("monitor:log:export") + @GetMapping("/export/login") + public void exportLoginLog(LogQuery query, SortQuery sortQuery, HttpServletResponse response) { + baseService.exportLoginLog(query, sortQuery, response); + } + + @ExcludeFromGracefulResponse + @Operation(summary = "导出操作日志", description = "导出操作日志") + @SaCheckPermission("monitor:log:export") + @GetMapping("/export/operation") + public void exportOperationLog(LogQuery query, SortQuery sortQuery, HttpServletResponse response) { + baseService.exportOperationLog(query, sortQuery, response); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/MenuController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/MenuController.java new file mode 100644 index 0000000..a06a2e8 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/MenuController.java @@ -0,0 +1,64 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.MenuQuery; +import top.wms.admin.system.model.req.MenuReq; +import top.wms.admin.system.model.resp.MenuResp; +import top.wms.admin.system.service.MenuService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.constant.StringConstants; +import top.continew.starter.core.util.URLUtils; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.extension.crud.annotation.CrudApi; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; + +import java.lang.reflect.Method; + +/** + * 菜单管理 API + * + * @author Charles7c + * @since 2023/2/15 20:35 + */ +@Tag(name = "菜单管理 API") +@RestController +@CrudRequestMapping(value = "/system/menu", api = {Api.TREE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) +public class MenuController extends BaseController { + + @Operation(summary = "清除缓存", description = "清除缓存") + @SaCheckPermission("system:menu:clearCache") + @DeleteMapping("/cache") + public void clearCache() { + RedisUtils.deleteByPattern(CacheConstants.ROLE_MENU_KEY_PREFIX + StringConstants.ASTERISK); + } + + @Override + public void preHandle(CrudApi crudApi, Object[] args, Method targetMethod, Class targetClass) throws Exception { + super.preHandle(crudApi, args, targetMethod, targetClass); + Api api = crudApi.value(); + if (!(Api.ADD.equals(api) || Api.UPDATE.equals(api))) { + return; + } + MenuReq req = (MenuReq)args[0]; + Boolean isExternal = ObjectUtil.defaultIfNull(req.getIsExternal(), false); + String path = req.getPath(); + ValidationUtils.throwIf(Boolean.TRUE.equals(isExternal) && !URLUtils + .isHttpUrl(path), "路由地址格式错误,请以 http:// 或 https:// 开头"); + // 非外链菜单参数修正 + if (Boolean.FALSE.equals(isExternal)) { + ValidationUtils.throwIf(URLUtils.isHttpUrl(path), "路由地址格式错误"); + req.setPath(StrUtil.isBlank(path) ? path : StrUtil.prependIfMissing(path, StringConstants.SLASH)); + req.setName(StrUtil.removePrefix(req.getName(), StringConstants.SLASH)); + req.setComponent(StrUtil.removePrefix(req.getComponent(), StringConstants.SLASH)); + } + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/OptionController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/OptionController.java new file mode 100644 index 0000000..79400f0 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/OptionController.java @@ -0,0 +1,53 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.system.model.query.OptionQuery; +import top.wms.admin.system.model.req.OptionReq; +import top.wms.admin.system.model.req.OptionResetValueReq; +import top.wms.admin.system.model.resp.OptionResp; +import top.wms.admin.system.service.OptionService; + +import java.util.List; + +/** + * 参数管理 API + * + * @author Bull-BCLS + * @since 2023/8/26 19:38 + */ +@Tag(name = "参数管理 API") +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/option") +public class OptionController { + + private final OptionService baseService; + + @Operation(summary = "查询参数列表", description = "查询参数列表") + @SaCheckPermission("system:config:list") + @GetMapping + public List list(@Validated OptionQuery query) { + return baseService.list(query); + } + + @Operation(summary = "修改参数", description = "修改参数") + @SaCheckPermission("system:config:update") + @PutMapping + public void update(@Valid @RequestBody List options) { + baseService.update(options); + } + + @Operation(summary = "重置参数", description = "重置参数") + @SaCheckPermission("system:config:reset") + @PatchMapping("/value") + public void resetValue(@Validated @RequestBody OptionResetValueReq req) { + baseService.resetValue(req); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/RoleController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/RoleController.java new file mode 100644 index 0000000..c1c0612 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/RoleController.java @@ -0,0 +1,84 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotEmpty; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.system.model.query.RoleQuery; +import top.wms.admin.system.model.query.RoleUserQuery; +import top.wms.admin.system.model.req.RoleReq; +import top.wms.admin.system.model.req.RoleUpdatePermissionReq; +import top.wms.admin.system.model.resp.role.RoleDetailResp; +import top.wms.admin.system.model.resp.role.RoleResp; +import top.wms.admin.system.model.resp.role.RoleUserResp; +import top.wms.admin.system.service.RoleService; +import top.wms.admin.system.service.UserRoleService; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; +import top.continew.starter.extension.crud.model.query.PageQuery; +import top.continew.starter.extension.crud.model.resp.PageResp; + +import java.util.List; + +/** + * 角色管理 API + * + * @author Charles7c + * @since 2023/2/8 23:11 + */ +@Tag(name = "角色管理 API") +@Validated +@RestController +@RequiredArgsConstructor +@CrudRequestMapping(value = "/system/role", api = {Api.LIST, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) +public class RoleController extends BaseController { + + private final UserRoleService userRoleService; + + @Operation(summary = "修改权限", description = "修改角色的功能权限") + @SaCheckPermission("system:role:updatePermission") + @PutMapping("/{id}/permission") + public void updatePermission(@PathVariable("id") Long id, @Validated @RequestBody RoleUpdatePermissionReq req) { + baseService.updatePermission(id, req); + } + + @Operation(summary = "分页查询关联用户", description = "分页查询角色关联的用户列表") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("system:role:list") + @GetMapping("/{id}/user") + public PageResp pageUser(@PathVariable("id") Long id, + @Validated RoleUserQuery query, + @Validated PageQuery pageQuery) { + query.setRoleId(id); + return userRoleService.pageUser(query, pageQuery); + } + + @Operation(summary = "分配用户", description = "批量分配角色给用户") + @SaCheckPermission("system:role:assign") + @PostMapping("/{id}/user") + public void assignToUsers(@PathVariable("id") Long id, + @Validated @NotEmpty(message = "用户ID列表不能为空") @RequestBody List userIds) { + baseService.assignToUsers(id, userIds); + } + + @Operation(summary = "取消分配用户", description = "批量取消分配角色给用户") + @SaCheckPermission("system:role:unassign") + @DeleteMapping("/user") + public void unassignFromUsers(@Validated @NotEmpty(message = "用户列表不能为空") @RequestBody List userRoleIds) { + userRoleService.deleteByIds(userRoleIds); + } + + @Operation(summary = "查询关联用户ID", description = "查询角色关联的用户ID列表") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("system:role:list") + @GetMapping("/{id}/user/id") + public List listUserId(@PathVariable("id") Long id) { + return userRoleService.listUserIdByRoleId(id); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/StorageController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/StorageController.java new file mode 100644 index 0000000..f9c003e --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/StorageController.java @@ -0,0 +1,49 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.common.model.req.CommonStatusUpdateReq; +import top.wms.admin.system.model.query.StorageQuery; +import top.wms.admin.system.model.req.StorageReq; +import top.wms.admin.system.model.resp.StorageResp; +import top.wms.admin.system.service.StorageService; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; + +/** + * 存储管理 API + * + * @author Charles7c + * @since 2023/12/26 22:09 + */ +@Tag(name = "存储管理 API") +@Validated +@RestController +@CrudRequestMapping(value = "/system/storage", api = {Api.LIST, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) +public class StorageController extends BaseController { + + @Operation(summary = "修改状态", description = "修改状态") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("system:storage:updateStatus") + @PutMapping({"/{id}/status"}) + public void updateStatus(@Validated @RequestBody CommonStatusUpdateReq req, @PathVariable("id") Long id) { + baseService.updateStatus(req, id); + } + + @Operation(summary = "设为默认存储", description = "设为默认存储") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("system:storage:setDefault") + @PutMapping({"/{id}/default"}) + public void setDefault(@PathVariable("id") Long id) { + baseService.setDefault(id); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/UserCenterController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/UserCenterController.java new file mode 100644 index 0000000..149a992 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/UserCenterController.java @@ -0,0 +1,132 @@ +package top.wms.admin.controller.system; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.common.constant.CacheConstants; +import top.wms.admin.common.context.UserContextHolder; +import top.wms.admin.common.util.SecureUtils; +import top.wms.admin.system.enums.SocialSourceEnum; +import top.wms.admin.system.model.entity.UserSocialDO; +import top.wms.admin.system.model.req.user.UserBasicInfoUpdateReq; +import top.wms.admin.system.model.req.user.UserEmailUpdateRequest; +import top.wms.admin.system.model.req.user.UserPasswordUpdateReq; +import top.wms.admin.system.model.req.user.UserPhoneUpdateReq; +import top.wms.admin.system.model.resp.AvatarResp; +import top.wms.admin.system.model.resp.user.UserSocialBindResp; +import top.wms.admin.system.service.UserService; +import top.wms.admin.system.service.UserSocialService; +import top.continew.starter.cache.redisson.util.RedisUtils; +import top.continew.starter.core.exception.BadRequestException; +import top.continew.starter.core.util.ExceptionUtils; +import top.continew.starter.core.validation.ValidationUtils; + +import java.io.IOException; +import java.util.List; + +/** + * 个人中心 API + * + * @author Charles7c + * @since 2023/1/2 11:41 + */ +@Tag(name = "个人中心 API") +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/user") +public class UserCenterController { + + private static final String DECRYPT_FAILED = "当前密码解密失败"; + private static final String CAPTCHA_EXPIRED = "验证码已失效"; + private final UserService userService; + private final UserSocialService userSocialService; + + @Operation(summary = "修改头像", description = "用户修改个人头像") + @PostMapping("/avatar") + public AvatarResp updateAvatar(@NotNull(message = "头像不能为空") MultipartFile avatarFile) throws IOException { + ValidationUtils.throwIf(avatarFile::isEmpty, "头像不能为空"); + String newAvatar = userService.updateAvatar(avatarFile, UserContextHolder.getUserId()); + return AvatarResp.builder().avatar(newAvatar).build(); + } + + @Operation(summary = "修改基础信息", description = "修改用户基础信息") + @PatchMapping("/basic/info") + public void updateBasicInfo(@Validated @RequestBody UserBasicInfoUpdateReq req) { + userService.updateBasicInfo(req, UserContextHolder.getUserId()); + } + + @Operation(summary = "修改密码", description = "修改用户登录密码") + @PatchMapping("/password") + public void updatePassword(@Validated @RequestBody UserPasswordUpdateReq updateReq) { + String rawOldPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateReq + .getOldPassword())); + ValidationUtils.throwIfNull(rawOldPassword, DECRYPT_FAILED); + String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateReq + .getNewPassword())); + ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); + userService.updatePassword(rawOldPassword, rawNewPassword, UserContextHolder.getUserId()); + } + + @Operation(summary = "修改手机号", description = "修改手机号") + @PatchMapping("/phone") + public void updatePhone(@Validated @RequestBody UserPhoneUpdateReq updateReq) { + String rawOldPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateReq + .getOldPassword())); + ValidationUtils.throwIfBlank(rawOldPassword, DECRYPT_FAILED); + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + updateReq.getPhone(); + String captcha = RedisUtils.get(captchaKey); + ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); + ValidationUtils.throwIfNotEqualIgnoreCase(updateReq.getCaptcha(), captcha, "验证码错误"); + RedisUtils.delete(captchaKey); + userService.updatePhone(updateReq.getPhone(), rawOldPassword, UserContextHolder.getUserId()); + } + + @Operation(summary = "修改邮箱", description = "修改用户邮箱") + @PatchMapping("/email") + public void updateEmail(@Validated @RequestBody UserEmailUpdateRequest updateReq) { + String rawOldPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateReq + .getOldPassword())); + ValidationUtils.throwIfBlank(rawOldPassword, DECRYPT_FAILED); + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + updateReq.getEmail(); + String captcha = RedisUtils.get(captchaKey); + ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); + ValidationUtils.throwIfNotEqualIgnoreCase(updateReq.getCaptcha(), captcha, "验证码错误"); + RedisUtils.delete(captchaKey); + userService.updateEmail(updateReq.getEmail(), rawOldPassword, UserContextHolder.getUserId()); + } + + @Operation(summary = "查询绑定的三方账号", description = "查询绑定的三方账号") + @GetMapping("/social") + public List listSocialBind() { + List userSocialList = userSocialService.listByUserId(UserContextHolder.getUserId()); + return userSocialList.stream().map(userSocial -> { + String source = userSocial.getSource(); + UserSocialBindResp userSocialBind = new UserSocialBindResp(); + userSocialBind.setSource(source); + userSocialBind.setDescription(SocialSourceEnum.valueOf(source).getDescription()); + return userSocialBind; + }).toList(); + } + + @Operation(summary = "绑定三方账号", description = "绑定三方账号") + @Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH) + @PostMapping("/social/{source}") + public void bindSocial(@PathVariable String source, @RequestBody Object callback) { + // 第三方登录已禁用 + throw new BadRequestException("第三方登录功能已禁用"); + } + + @Operation(summary = "解绑三方账号", description = "解绑三方账号") + @Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH) + @DeleteMapping("/social/{source}") + public void unbindSocial(@PathVariable String source) { + userSocialService.deleteBySourceAndUserId(source, UserContextHolder.getUserId()); + } +} \ No newline at end of file diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/UserController.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/UserController.java new file mode 100644 index 0000000..edd39bd --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/UserController.java @@ -0,0 +1,107 @@ +package top.wms.admin.controller.system; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.ReUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import top.wms.admin.common.controller.BaseController; +import top.wms.admin.common.constant.RegexConstants; +import top.wms.admin.common.util.SecureUtils; +import top.wms.admin.system.model.query.UserQuery; +import top.wms.admin.system.model.req.user.UserImportReq; +import top.wms.admin.system.model.req.user.UserPasswordResetReq; +import top.wms.admin.system.model.req.user.UserReq; +import top.wms.admin.system.model.req.user.UserRoleUpdateReq; +import top.wms.admin.system.model.resp.user.UserDetailResp; +import top.wms.admin.system.model.resp.user.UserImportParseResp; +import top.wms.admin.system.model.resp.user.UserImportResp; +import top.wms.admin.system.model.resp.user.UserResp; +import top.wms.admin.system.service.UserService; +import top.continew.starter.core.util.ExceptionUtils; +import top.continew.starter.core.validation.ValidationUtils; +import top.continew.starter.extension.crud.annotation.CrudRequestMapping; +import top.continew.starter.extension.crud.enums.Api; +import top.continew.starter.extension.crud.model.resp.BaseIdResp; +import top.continew.starter.extension.crud.validation.CrudValidationGroup; + +import java.io.IOException; + +/** + * 用户管理 API + * + * @author Charles7c + * @since 2023/2/20 21:00 + */ +@Tag(name = "用户管理 API") +@Validated +@RestController +@RequiredArgsConstructor +@CrudRequestMapping(value = "/system/user", api = {Api.PAGE, Api.LIST, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, + Api.EXPORT}) +public class UserController extends BaseController { + + @Override + @Operation(summary = "新增数据", description = "新增数据") + public BaseIdResp add(@Validated(CrudValidationGroup.Add.class) @RequestBody UserReq req) { + String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); + ValidationUtils.throwIfNull(rawPassword, "密码解密失败"); + ValidationUtils.throwIf(!ReUtil + .isMatch(RegexConstants.PASSWORD, rawPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字"); + req.setPassword(rawPassword); + return super.add(req); + } + + @Operation(summary = "下载导入模板", description = "下载导入模板") + @SaCheckPermission("system:user:import") + @GetMapping(value = "/import/template", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public void downloadImportTemplate(HttpServletResponse response) throws IOException { + baseService.downloadImportTemplate(response); + } + + @Operation(summary = "解析导入数据", description = "解析导入数据") + @SaCheckPermission("system:user:import") + @PostMapping("/import/parse") + public UserImportParseResp parseImport(@NotNull(message = "文件不能为空") MultipartFile file) { + ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); + return baseService.parseImport(file); + } + + @Operation(summary = "导入数据", description = "导入数据") + @SaCheckPermission("system:user:import") + @PostMapping(value = "/import") + public UserImportResp importUser(@Validated @RequestBody UserImportReq req) { + return baseService.importUser(req); + } + + @Operation(summary = "重置密码", description = "重置用户登录密码") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("system:user:resetPwd") + @PatchMapping("/{id}/password") + public void resetPassword(@Validated @RequestBody UserPasswordResetReq req, @PathVariable Long id) { + String rawNewPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getNewPassword())); + ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); + ValidationUtils.throwIf(!ReUtil + .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字"); + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + req.setNewPassword("{bcrypt}" + encoder.encode(rawNewPassword)); + baseService.resetPassword(req, id); + } + + @Operation(summary = "分配角色", description = "为用户新增或移除角色") + @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) + @SaCheckPermission("system:user:updateRole") + @PatchMapping("/{id}/role") + public void updateRole(@Validated @RequestBody UserRoleUpdateReq updateReq, @PathVariable Long id) { + baseService.updateRole(updateReq, id); + } +} diff --git a/mes-webapi/src/main/java/top/mes/admin/controller/system/zctest.java b/mes-webapi/src/main/java/top/mes/admin/controller/system/zctest.java new file mode 100644 index 0000000..4197047 --- /dev/null +++ b/mes-webapi/src/main/java/top/mes/admin/controller/system/zctest.java @@ -0,0 +1,40 @@ +package top.wms.admin.controller.system; + +import org.dromara.x.file.storage.core.FileInfo; +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 top.wms.admin.common.util.PictureUtils; +// import top.wms.admin.controller.file.SysFileController; +// import top.wms.admin.file.domain.SysFile; +import top.wms.admin.system.model.resp.file.FileUploadResp; +import top.wms.admin.system.service.FileService; + +@RestController +@RequestMapping("/test") +public class zctest { + + // @Autowired + // private SysFileController fileController; + + @Autowired + private FileService fileService; + + @PostMapping() + public FileUploadResp test() { + String urls = "http://bpic.588ku.com/element_origin_min_pic/19/03/15/75076c485081d15ed9c224ad3e4ce4a1.jpg"; + MultipartFile file = PictureUtils.createMultipartFile(urls, "zctest11111.jpg"); + // SysFile sysFile = fileController.uploadMinio(file, "zctest11111"); + // return sysFile; + + FileInfo fileInfo = fileService.upload(file, "zctest", "minio", false, false); + return FileUploadResp.builder() + .id(fileInfo.getId()) + .url(fileInfo.getUrl()) + .thUrl(fileInfo.getThUrl()) + .metadata(fileInfo.getMetadata()) + .build(); + } +} diff --git a/mes-webapi/src/main/resources/banner.txt b/mes-webapi/src/main/resources/banner.txt new file mode 100644 index 0000000..302f8af --- /dev/null +++ b/mes-webapi/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + ____ _ _ _ _ _ _ _ + / ___| ___ _ __ | |_ (_)| \ | | ___ __ __ / \ __| | _ __ ___ (_) _ __ + | | / _ \ | '_ \ | __|| || \| | / _ \\ \ /\ / /_____ / _ \ / _` || '_ ` _ \ | || '_ \ + | |___| (_) || | | || |_ | || |\ || __/ \ V V /|_____|/ ___ \| (_| || | | | | || || | | | + \____|\___/ |_| |_| \__||_||_| \_| \___| \_/\_/ /_/ \_\\__,_||_| |_| |_||_||_| |_| + + :: ${project.name} :: v${project.version} + :: ContiNew Starter :: v2.9.0 + :: Spring Boot :: v${spring-boot.version} diff --git a/mes-webapi/src/main/resources/china.json b/mes-webapi/src/main/resources/china.json new file mode 100644 index 0000000..c16c767 --- /dev/null +++ b/mes-webapi/src/main/resources/china.json @@ -0,0 +1 @@ +{"code":100000,"name":"中国","fullname":"中华人民共和国","level":"country","filename":"100000","children":[{"code":110000,"name":"北京","fullname":"北京市","level":"province","filename":"110000","children":[{"code":110101,"name":"东城","fullname":"东城区","level":"district","filename":"110000/110101"},{"code":110102,"name":"西城","fullname":"西城区","level":"district","filename":"110000/110102"},{"code":110105,"name":"朝阳","fullname":"朝阳区","level":"district","filename":"110000/110105"},{"code":110106,"name":"丰台","fullname":"丰台区","level":"district","filename":"110000/110106"},{"code":110107,"name":"石景山","fullname":"石景山区","level":"district","filename":"110000/110107"},{"code":110108,"name":"海淀","fullname":"海淀区","level":"district","filename":"110000/110108"},{"code":110109,"name":"门头沟","fullname":"门头沟区","level":"district","filename":"110000/110109"},{"code":110111,"name":"房山","fullname":"房山区","level":"district","filename":"110000/110111"},{"code":110112,"name":"通州","fullname":"通州区","level":"district","filename":"110000/110112"},{"code":110113,"name":"顺义","fullname":"顺义区","level":"district","filename":"110000/110113"},{"code":110114,"name":"昌平","fullname":"昌平区","level":"district","filename":"110000/110114"},{"code":110115,"name":"大兴","fullname":"大兴区","level":"district","filename":"110000/110115"},{"code":110116,"name":"怀柔","fullname":"怀柔区","level":"district","filename":"110000/110116"},{"code":110117,"name":"平谷","fullname":"平谷区","level":"district","filename":"110000/110117"},{"code":110118,"name":"密云","fullname":"密云区","level":"district","filename":"110000/110118"},{"code":110119,"name":"延庆","fullname":"延庆区","level":"district","filename":"110000/110119"}]},{"code":120000,"name":"天津","fullname":"天津市","level":"province","filename":"120000","children":[{"code":120101,"name":"和平","fullname":"和平区","level":"district","filename":"120000/120101"},{"code":120102,"name":"河东","fullname":"河东区","level":"district","filename":"120000/120102"},{"code":120103,"name":"河西","fullname":"河西区","level":"district","filename":"120000/120103"},{"code":120104,"name":"南开","fullname":"南开区","level":"district","filename":"120000/120104"},{"code":120105,"name":"河北","fullname":"河北区","level":"district","filename":"120000/120105"},{"code":120106,"name":"红桥","fullname":"红桥区","level":"district","filename":"120000/120106"},{"code":120110,"name":"东丽","fullname":"东丽区","level":"district","filename":"120000/120110"},{"code":120111,"name":"西青","fullname":"西青区","level":"district","filename":"120000/120111"},{"code":120112,"name":"津南","fullname":"津南区","level":"district","filename":"120000/120112"},{"code":120113,"name":"北辰","fullname":"北辰区","level":"district","filename":"120000/120113"},{"code":120114,"name":"武清","fullname":"武清区","level":"district","filename":"120000/120114"},{"code":120115,"name":"宝坻","fullname":"宝坻区","level":"district","filename":"120000/120115"},{"code":120116,"name":"滨海","fullname":"滨海新区","level":"district","filename":"120000/120116"},{"code":120117,"name":"宁河","fullname":"宁河区","level":"district","filename":"120000/120117"},{"code":120118,"name":"静海","fullname":"静海区","level":"district","filename":"120000/120118"},{"code":120119,"name":"蓟州","fullname":"蓟州区","level":"district","filename":"120000/120119"}]},{"code":130000,"name":"河北","fullname":"河北省","level":"province","filename":"130000","children":[{"code":130100,"name":"石家庄","fullname":"石家庄市","level":"city","filename":"130000/130100","children":[{"code":130102,"name":"长安","fullname":"长安区","level":"district","filename":"130000/130100/130102"},{"code":130104,"name":"桥西","fullname":"桥西区","level":"district","filename":"130000/130100/130104"},{"code":130105,"name":"新华","fullname":"新华区","level":"district","filename":"130000/130100/130105"},{"code":130107,"name":"井陉","fullname":"井陉矿区","level":"district","filename":"130000/130100/130107"},{"code":130108,"name":"裕华","fullname":"裕华区","level":"district","filename":"130000/130100/130108"},{"code":130109,"name":"藁城","fullname":"藁城区","level":"district","filename":"130000/130100/130109"},{"code":130110,"name":"鹿泉","fullname":"鹿泉区","level":"district","filename":"130000/130100/130110"},{"code":130111,"name":"栾城","fullname":"栾城区","level":"district","filename":"130000/130100/130111"},{"code":130121,"name":"井陉","fullname":"井陉县","level":"district","filename":"130000/130100/130121"},{"code":130123,"name":"正定","fullname":"正定县","level":"district","filename":"130000/130100/130123"},{"code":130125,"name":"行唐","fullname":"行唐县","level":"district","filename":"130000/130100/130125"},{"code":130126,"name":"灵寿","fullname":"灵寿县","level":"district","filename":"130000/130100/130126"},{"code":130127,"name":"高邑","fullname":"高邑县","level":"district","filename":"130000/130100/130127"},{"code":130128,"name":"深泽","fullname":"深泽县","level":"district","filename":"130000/130100/130128"},{"code":130129,"name":"赞皇","fullname":"赞皇县","level":"district","filename":"130000/130100/130129"},{"code":130130,"name":"无极","fullname":"无极县","level":"district","filename":"130000/130100/130130"},{"code":130131,"name":"平山","fullname":"平山县","level":"district","filename":"130000/130100/130131"},{"code":130132,"name":"元氏","fullname":"元氏县","level":"district","filename":"130000/130100/130132"},{"code":130133,"name":"赵县","fullname":"赵县","level":"district","filename":"130000/130100/130133"},{"code":130181,"name":"辛集","fullname":"辛集市","level":"district","filename":"130000/130100/130181"},{"code":130183,"name":"晋州","fullname":"晋州市","level":"district","filename":"130000/130100/130183"},{"code":130184,"name":"新乐","fullname":"新乐市","level":"district","filename":"130000/130100/130184"}]},{"code":130200,"name":"唐山","fullname":"唐山市","level":"city","filename":"130000/130200","children":[{"code":130202,"name":"路南","fullname":"路南区","level":"district","filename":"130000/130200/130202"},{"code":130203,"name":"路北","fullname":"路北区","level":"district","filename":"130000/130200/130203"},{"code":130204,"name":"古冶","fullname":"古冶区","level":"district","filename":"130000/130200/130204"},{"code":130205,"name":"开平","fullname":"开平区","level":"district","filename":"130000/130200/130205"},{"code":130207,"name":"丰南","fullname":"丰南区","level":"district","filename":"130000/130200/130207"},{"code":130208,"name":"丰润","fullname":"丰润区","level":"district","filename":"130000/130200/130208"},{"code":130209,"name":"曹妃甸","fullname":"曹妃甸区","level":"district","filename":"130000/130200/130209"},{"code":130224,"name":"滦南","fullname":"滦南县","level":"district","filename":"130000/130200/130224"},{"code":130225,"name":"乐亭","fullname":"乐亭县","level":"district","filename":"130000/130200/130225"},{"code":130227,"name":"迁西","fullname":"迁西县","level":"district","filename":"130000/130200/130227"},{"code":130229,"name":"玉田","fullname":"玉田县","level":"district","filename":"130000/130200/130229"},{"code":130281,"name":"遵化","fullname":"遵化市","level":"district","filename":"130000/130200/130281"},{"code":130283,"name":"迁安","fullname":"迁安市","level":"district","filename":"130000/130200/130283"},{"code":130284,"name":"滦州","fullname":"滦州市","level":"district","filename":"130000/130200/130284"}]},{"code":130300,"name":"秦皇岛","fullname":"秦皇岛市","level":"city","filename":"130000/130300","children":[{"code":130302,"name":"海港","fullname":"海港区","level":"district","filename":"130000/130300/130302"},{"code":130303,"name":"山海关","fullname":"山海关区","level":"district","filename":"130000/130300/130303"},{"code":130304,"name":"北戴河","fullname":"北戴河区","level":"district","filename":"130000/130300/130304"},{"code":130306,"name":"抚宁","fullname":"抚宁区","level":"district","filename":"130000/130300/130306"},{"code":130321,"name":"青龙","fullname":"青龙满族自治县","level":"district","filename":"130000/130300/130321"},{"code":130322,"name":"昌黎","fullname":"昌黎县","level":"district","filename":"130000/130300/130322"},{"code":130324,"name":"卢龙","fullname":"卢龙县","level":"district","filename":"130000/130300/130324"}]},{"code":130400,"name":"邯郸","fullname":"邯郸市","level":"city","filename":"130000/130400","children":[{"code":130402,"name":"邯山","fullname":"邯山区","level":"district","filename":"130000/130400/130402"},{"code":130403,"name":"丛台","fullname":"丛台区","level":"district","filename":"130000/130400/130403"},{"code":130404,"name":"复兴","fullname":"复兴区","level":"district","filename":"130000/130400/130404"},{"code":130406,"name":"峰峰","fullname":"峰峰矿区","level":"district","filename":"130000/130400/130406"},{"code":130407,"name":"肥乡","fullname":"肥乡区","level":"district","filename":"130000/130400/130407"},{"code":130408,"name":"永年","fullname":"永年区","level":"district","filename":"130000/130400/130408"},{"code":130423,"name":"临漳","fullname":"临漳县","level":"district","filename":"130000/130400/130423"},{"code":130424,"name":"成安","fullname":"成安县","level":"district","filename":"130000/130400/130424"},{"code":130425,"name":"大名","fullname":"大名县","level":"district","filename":"130000/130400/130425"},{"code":130426,"name":"涉县","fullname":"涉县","level":"district","filename":"130000/130400/130426"},{"code":130427,"name":"磁县","fullname":"磁县","level":"district","filename":"130000/130400/130427"},{"code":130430,"name":"邱县","fullname":"邱县","level":"district","filename":"130000/130400/130430"},{"code":130431,"name":"鸡泽","fullname":"鸡泽县","level":"district","filename":"130000/130400/130431"},{"code":130432,"name":"广平","fullname":"广平县","level":"district","filename":"130000/130400/130432"},{"code":130433,"name":"馆陶","fullname":"馆陶县","level":"district","filename":"130000/130400/130433"},{"code":130434,"name":"魏县","fullname":"魏县","level":"district","filename":"130000/130400/130434"},{"code":130435,"name":"曲周","fullname":"曲周县","level":"district","filename":"130000/130400/130435"},{"code":130481,"name":"武安","fullname":"武安市","level":"district","filename":"130000/130400/130481"}]},{"code":130500,"name":"邢台","fullname":"邢台市","level":"city","filename":"130000/130500","children":[{"code":130502,"name":"襄都","fullname":"襄都区","level":"district","filename":"130000/130500/130502"},{"code":130503,"name":"信都","fullname":"信都区","level":"district","filename":"130000/130500/130503"},{"code":130505,"name":"任泽","fullname":"任泽区","level":"district","filename":"130000/130500/130505"},{"code":130506,"name":"南和","fullname":"南和区","level":"district","filename":"130000/130500/130506"},{"code":130522,"name":"临城","fullname":"临城县","level":"district","filename":"130000/130500/130522"},{"code":130523,"name":"内丘","fullname":"内丘县","level":"district","filename":"130000/130500/130523"},{"code":130524,"name":"柏乡","fullname":"柏乡县","level":"district","filename":"130000/130500/130524"},{"code":130525,"name":"隆尧","fullname":"隆尧县","level":"district","filename":"130000/130500/130525"},{"code":130528,"name":"宁晋","fullname":"宁晋县","level":"district","filename":"130000/130500/130528"},{"code":130529,"name":"巨鹿","fullname":"巨鹿县","level":"district","filename":"130000/130500/130529"},{"code":130530,"name":"新河","fullname":"新河县","level":"district","filename":"130000/130500/130530"},{"code":130531,"name":"广宗","fullname":"广宗县","level":"district","filename":"130000/130500/130531"},{"code":130532,"name":"平乡","fullname":"平乡县","level":"district","filename":"130000/130500/130532"},{"code":130533,"name":"威县","fullname":"威县","level":"district","filename":"130000/130500/130533"},{"code":130534,"name":"清河","fullname":"清河县","level":"district","filename":"130000/130500/130534"},{"code":130535,"name":"临西","fullname":"临西县","level":"district","filename":"130000/130500/130535"},{"code":130581,"name":"南宫","fullname":"南宫市","level":"district","filename":"130000/130500/130581"},{"code":130582,"name":"沙河","fullname":"沙河市","level":"district","filename":"130000/130500/130582"}]},{"code":130600,"name":"保定","fullname":"保定市","level":"city","filename":"130000/130600","children":[{"code":130602,"name":"竞秀","fullname":"竞秀区","level":"district","filename":"130000/130600/130602"},{"code":130606,"name":"莲池","fullname":"莲池区","level":"district","filename":"130000/130600/130606"},{"code":130607,"name":"满城","fullname":"满城区","level":"district","filename":"130000/130600/130607"},{"code":130608,"name":"清苑","fullname":"清苑区","level":"district","filename":"130000/130600/130608"},{"code":130609,"name":"徐水","fullname":"徐水区","level":"district","filename":"130000/130600/130609"},{"code":130623,"name":"涞水","fullname":"涞水县","level":"district","filename":"130000/130600/130623"},{"code":130624,"name":"阜平","fullname":"阜平县","level":"district","filename":"130000/130600/130624"},{"code":130626,"name":"定兴","fullname":"定兴县","level":"district","filename":"130000/130600/130626"},{"code":130627,"name":"唐县","fullname":"唐县","level":"district","filename":"130000/130600/130627"},{"code":130628,"name":"高阳","fullname":"高阳县","level":"district","filename":"130000/130600/130628"},{"code":130629,"name":"容城","fullname":"容城县","level":"district","filename":"130000/130600/130629"},{"code":130630,"name":"涞源","fullname":"涞源县","level":"district","filename":"130000/130600/130630"},{"code":130631,"name":"望都","fullname":"望都县","level":"district","filename":"130000/130600/130631"},{"code":130632,"name":"安新","fullname":"安新县","level":"district","filename":"130000/130600/130632"},{"code":130633,"name":"易县","fullname":"易县","level":"district","filename":"130000/130600/130633"},{"code":130634,"name":"曲阳","fullname":"曲阳县","level":"district","filename":"130000/130600/130634"},{"code":130635,"name":"蠡县","fullname":"蠡县","level":"district","filename":"130000/130600/130635"},{"code":130636,"name":"顺平","fullname":"顺平县","level":"district","filename":"130000/130600/130636"},{"code":130637,"name":"博野","fullname":"博野县","level":"district","filename":"130000/130600/130637"},{"code":130638,"name":"雄县","fullname":"雄县","level":"district","filename":"130000/130600/130638"},{"code":130681,"name":"涿州","fullname":"涿州市","level":"district","filename":"130000/130600/130681"},{"code":130682,"name":"定州","fullname":"定州市","level":"district","filename":"130000/130600/130682"},{"code":130683,"name":"安国","fullname":"安国市","level":"district","filename":"130000/130600/130683"},{"code":130684,"name":"高碑店","fullname":"高碑店市","level":"district","filename":"130000/130600/130684"}]},{"code":130700,"name":"张家口","fullname":"张家口市","level":"city","filename":"130000/130700","children":[{"code":130702,"name":"桥东","fullname":"桥东区","level":"district","filename":"130000/130700/130702"},{"code":130703,"name":"桥西","fullname":"桥西区","level":"district","filename":"130000/130700/130703"},{"code":130705,"name":"宣化","fullname":"宣化区","level":"district","filename":"130000/130700/130705"},{"code":130706,"name":"下花园","fullname":"下花园区","level":"district","filename":"130000/130700/130706"},{"code":130708,"name":"万全","fullname":"万全区","level":"district","filename":"130000/130700/130708"},{"code":130709,"name":"崇礼","fullname":"崇礼区","level":"district","filename":"130000/130700/130709"},{"code":130722,"name":"张北","fullname":"张北县","level":"district","filename":"130000/130700/130722"},{"code":130723,"name":"康保","fullname":"康保县","level":"district","filename":"130000/130700/130723"},{"code":130724,"name":"沽源","fullname":"沽源县","level":"district","filename":"130000/130700/130724"},{"code":130725,"name":"尚义","fullname":"尚义县","level":"district","filename":"130000/130700/130725"},{"code":130726,"name":"蔚县","fullname":"蔚县","level":"district","filename":"130000/130700/130726"},{"code":130727,"name":"阳原","fullname":"阳原县","level":"district","filename":"130000/130700/130727"},{"code":130728,"name":"怀安","fullname":"怀安县","level":"district","filename":"130000/130700/130728"},{"code":130730,"name":"怀来","fullname":"怀来县","level":"district","filename":"130000/130700/130730"},{"code":130731,"name":"涿鹿","fullname":"涿鹿县","level":"district","filename":"130000/130700/130731"},{"code":130732,"name":"赤城","fullname":"赤城县","level":"district","filename":"130000/130700/130732"}]},{"code":130800,"name":"承德","fullname":"承德市","level":"city","filename":"130000/130800","children":[{"code":130802,"name":"双桥","fullname":"双桥区","level":"district","filename":"130000/130800/130802"},{"code":130803,"name":"双滦","fullname":"双滦区","level":"district","filename":"130000/130800/130803"},{"code":130804,"name":"鹰手营子","fullname":"鹰手营子矿区","level":"district","filename":"130000/130800/130804"},{"code":130821,"name":"承德","fullname":"承德县","level":"district","filename":"130000/130800/130821"},{"code":130822,"name":"兴隆","fullname":"兴隆县","level":"district","filename":"130000/130800/130822"},{"code":130824,"name":"滦平","fullname":"滦平县","level":"district","filename":"130000/130800/130824"},{"code":130825,"name":"隆化","fullname":"隆化县","level":"district","filename":"130000/130800/130825"},{"code":130826,"name":"丰宁","fullname":"丰宁满族自治县","level":"district","filename":"130000/130800/130826"},{"code":130827,"name":"宽城","fullname":"宽城满族自治县","level":"district","filename":"130000/130800/130827"},{"code":130828,"name":"围场","fullname":"围场满族蒙古族自治县","level":"district","filename":"130000/130800/130828"},{"code":130881,"name":"平泉","fullname":"平泉市","level":"district","filename":"130000/130800/130881"}]},{"code":130900,"name":"沧州","fullname":"沧州市","level":"city","filename":"130000/130900","children":[{"code":130902,"name":"新华","fullname":"新华区","level":"district","filename":"130000/130900/130902"},{"code":130903,"name":"运河","fullname":"运河区","level":"district","filename":"130000/130900/130903"},{"code":130921,"name":"沧县","fullname":"沧县","level":"district","filename":"130000/130900/130921"},{"code":130922,"name":"青县","fullname":"青县","level":"district","filename":"130000/130900/130922"},{"code":130923,"name":"东光","fullname":"东光县","level":"district","filename":"130000/130900/130923"},{"code":130924,"name":"海兴","fullname":"海兴县","level":"district","filename":"130000/130900/130924"},{"code":130925,"name":"盐山","fullname":"盐山县","level":"district","filename":"130000/130900/130925"},{"code":130926,"name":"肃宁","fullname":"肃宁县","level":"district","filename":"130000/130900/130926"},{"code":130927,"name":"南皮","fullname":"南皮县","level":"district","filename":"130000/130900/130927"},{"code":130928,"name":"吴桥","fullname":"吴桥县","level":"district","filename":"130000/130900/130928"},{"code":130929,"name":"献县","fullname":"献县","level":"district","filename":"130000/130900/130929"},{"code":130930,"name":"孟村","fullname":"孟村回族自治县","level":"district","filename":"130000/130900/130930"},{"code":130981,"name":"泊头","fullname":"泊头市","level":"district","filename":"130000/130900/130981"},{"code":130982,"name":"任丘","fullname":"任丘市","level":"district","filename":"130000/130900/130982"},{"code":130983,"name":"黄骅","fullname":"黄骅市","level":"district","filename":"130000/130900/130983"},{"code":130984,"name":"河间","fullname":"河间市","level":"district","filename":"130000/130900/130984"}]},{"code":131000,"name":"廊坊","fullname":"廊坊市","level":"city","filename":"130000/131000","children":[{"code":131002,"name":"安次","fullname":"安次区","level":"district","filename":"130000/131000/131002"},{"code":131003,"name":"广阳","fullname":"广阳区","level":"district","filename":"130000/131000/131003"},{"code":131022,"name":"固安","fullname":"固安县","level":"district","filename":"130000/131000/131022"},{"code":131023,"name":"永清","fullname":"永清县","level":"district","filename":"130000/131000/131023"},{"code":131024,"name":"香河","fullname":"香河县","level":"district","filename":"130000/131000/131024"},{"code":131025,"name":"大城","fullname":"大城县","level":"district","filename":"130000/131000/131025"},{"code":131026,"name":"文安","fullname":"文安县","level":"district","filename":"130000/131000/131026"},{"code":131028,"name":"大厂","fullname":"大厂回族自治县","level":"district","filename":"130000/131000/131028"},{"code":131081,"name":"霸州","fullname":"霸州市","level":"district","filename":"130000/131000/131081"},{"code":131082,"name":"三河","fullname":"三河市","level":"district","filename":"130000/131000/131082"}]},{"code":131100,"name":"衡水","fullname":"衡水市","level":"city","filename":"130000/131100","children":[{"code":131102,"name":"桃城","fullname":"桃城区","level":"district","filename":"130000/131100/131102"},{"code":131103,"name":"冀州","fullname":"冀州区","level":"district","filename":"130000/131100/131103"},{"code":131121,"name":"枣强","fullname":"枣强县","level":"district","filename":"130000/131100/131121"},{"code":131122,"name":"武邑","fullname":"武邑县","level":"district","filename":"130000/131100/131122"},{"code":131123,"name":"武强","fullname":"武强县","level":"district","filename":"130000/131100/131123"},{"code":131124,"name":"饶阳","fullname":"饶阳县","level":"district","filename":"130000/131100/131124"},{"code":131125,"name":"安平","fullname":"安平县","level":"district","filename":"130000/131100/131125"},{"code":131126,"name":"故城","fullname":"故城县","level":"district","filename":"130000/131100/131126"},{"code":131127,"name":"景县","fullname":"景县","level":"district","filename":"130000/131100/131127"},{"code":131128,"name":"阜城","fullname":"阜城县","level":"district","filename":"130000/131100/131128"},{"code":131182,"name":"深州","fullname":"深州市","level":"district","filename":"130000/131100/131182"}]}]},{"code":140000,"name":"山西","fullname":"山西省","level":"province","filename":"140000","children":[{"code":140100,"name":"太原","fullname":"太原市","level":"city","filename":"140000/140100","children":[{"code":140105,"name":"小店","fullname":"小店区","level":"district","filename":"140000/140100/140105"},{"code":140106,"name":"迎泽","fullname":"迎泽区","level":"district","filename":"140000/140100/140106"},{"code":140107,"name":"杏花岭","fullname":"杏花岭区","level":"district","filename":"140000/140100/140107"},{"code":140108,"name":"尖草坪","fullname":"尖草坪区","level":"district","filename":"140000/140100/140108"},{"code":140109,"name":"万柏林","fullname":"万柏林区","level":"district","filename":"140000/140100/140109"},{"code":140110,"name":"晋源","fullname":"晋源区","level":"district","filename":"140000/140100/140110"},{"code":140121,"name":"清徐","fullname":"清徐县","level":"district","filename":"140000/140100/140121"},{"code":140122,"name":"阳曲","fullname":"阳曲县","level":"district","filename":"140000/140100/140122"},{"code":140123,"name":"娄烦","fullname":"娄烦县","level":"district","filename":"140000/140100/140123"},{"code":140181,"name":"古交","fullname":"古交市","level":"district","filename":"140000/140100/140181"}]},{"code":140200,"name":"大同","fullname":"大同市","level":"city","filename":"140000/140200","children":[{"code":140212,"name":"新荣","fullname":"新荣区","level":"district","filename":"140000/140200/140212"},{"code":140213,"name":"平城","fullname":"平城区","level":"district","filename":"140000/140200/140213"},{"code":140214,"name":"云冈","fullname":"云冈区","level":"district","filename":"140000/140200/140214"},{"code":140215,"name":"云州","fullname":"云州区","level":"district","filename":"140000/140200/140215"},{"code":140221,"name":"阳高","fullname":"阳高县","level":"district","filename":"140000/140200/140221"},{"code":140222,"name":"天镇","fullname":"天镇县","level":"district","filename":"140000/140200/140222"},{"code":140223,"name":"广灵","fullname":"广灵县","level":"district","filename":"140000/140200/140223"},{"code":140224,"name":"灵丘","fullname":"灵丘县","level":"district","filename":"140000/140200/140224"},{"code":140225,"name":"浑源","fullname":"浑源县","level":"district","filename":"140000/140200/140225"},{"code":140226,"name":"左云","fullname":"左云县","level":"district","filename":"140000/140200/140226"}]},{"code":140300,"name":"阳泉","fullname":"阳泉市","level":"city","filename":"140000/140300","children":[{"code":140302,"name":"城区","fullname":"城区","level":"district","filename":"140000/140300/140302"},{"code":140303,"name":"矿区","fullname":"矿区","level":"district","filename":"140000/140300/140303"},{"code":140311,"name":"郊区","fullname":"郊区","level":"district","filename":"140000/140300/140311"},{"code":140321,"name":"平定","fullname":"平定县","level":"district","filename":"140000/140300/140321"},{"code":140322,"name":"盂县","fullname":"盂县","level":"district","filename":"140000/140300/140322"}]},{"code":140400,"name":"长治","fullname":"长治市","level":"city","filename":"140000/140400","children":[{"code":140403,"name":"潞州","fullname":"潞州区","level":"district","filename":"140000/140400/140403"},{"code":140404,"name":"上党","fullname":"上党区","level":"district","filename":"140000/140400/140404"},{"code":140405,"name":"屯留","fullname":"屯留区","level":"district","filename":"140000/140400/140405"},{"code":140406,"name":"潞城","fullname":"潞城区","level":"district","filename":"140000/140400/140406"},{"code":140423,"name":"襄垣","fullname":"襄垣县","level":"district","filename":"140000/140400/140423"},{"code":140425,"name":"平顺","fullname":"平顺县","level":"district","filename":"140000/140400/140425"},{"code":140426,"name":"黎城","fullname":"黎城县","level":"district","filename":"140000/140400/140426"},{"code":140427,"name":"壶关","fullname":"壶关县","level":"district","filename":"140000/140400/140427"},{"code":140428,"name":"长子","fullname":"长子县","level":"district","filename":"140000/140400/140428"},{"code":140429,"name":"武乡","fullname":"武乡县","level":"district","filename":"140000/140400/140429"},{"code":140430,"name":"沁县","fullname":"沁县","level":"district","filename":"140000/140400/140430"},{"code":140431,"name":"沁源","fullname":"沁源县","level":"district","filename":"140000/140400/140431"}]},{"code":140500,"name":"晋城","fullname":"晋城市","level":"city","filename":"140000/140500","children":[{"code":140502,"name":"城区","fullname":"城区","level":"district","filename":"140000/140500/140502"},{"code":140521,"name":"沁水","fullname":"沁水县","level":"district","filename":"140000/140500/140521"},{"code":140522,"name":"阳城","fullname":"阳城县","level":"district","filename":"140000/140500/140522"},{"code":140524,"name":"陵川","fullname":"陵川县","level":"district","filename":"140000/140500/140524"},{"code":140525,"name":"泽州","fullname":"泽州县","level":"district","filename":"140000/140500/140525"},{"code":140581,"name":"高平","fullname":"高平市","level":"district","filename":"140000/140500/140581"}]},{"code":140600,"name":"朔州","fullname":"朔州市","level":"city","filename":"140000/140600","children":[{"code":140602,"name":"朔城","fullname":"朔城区","level":"district","filename":"140000/140600/140602"},{"code":140603,"name":"平鲁","fullname":"平鲁区","level":"district","filename":"140000/140600/140603"},{"code":140621,"name":"山阴","fullname":"山阴县","level":"district","filename":"140000/140600/140621"},{"code":140622,"name":"应县","fullname":"应县","level":"district","filename":"140000/140600/140622"},{"code":140623,"name":"右玉","fullname":"右玉县","level":"district","filename":"140000/140600/140623"},{"code":140681,"name":"怀仁","fullname":"怀仁市","level":"district","filename":"140000/140600/140681"}]},{"code":140700,"name":"晋中","fullname":"晋中市","level":"city","filename":"140000/140700","children":[{"code":140702,"name":"榆次","fullname":"榆次区","level":"district","filename":"140000/140700/140702"},{"code":140703,"name":"太谷","fullname":"太谷区","level":"district","filename":"140000/140700/140703"},{"code":140721,"name":"榆社","fullname":"榆社县","level":"district","filename":"140000/140700/140721"},{"code":140722,"name":"左权","fullname":"左权县","level":"district","filename":"140000/140700/140722"},{"code":140723,"name":"和顺","fullname":"和顺县","level":"district","filename":"140000/140700/140723"},{"code":140724,"name":"昔阳","fullname":"昔阳县","level":"district","filename":"140000/140700/140724"},{"code":140725,"name":"寿阳","fullname":"寿阳县","level":"district","filename":"140000/140700/140725"},{"code":140727,"name":"祁县","fullname":"祁县","level":"district","filename":"140000/140700/140727"},{"code":140728,"name":"平遥","fullname":"平遥县","level":"district","filename":"140000/140700/140728"},{"code":140729,"name":"灵石","fullname":"灵石县","level":"district","filename":"140000/140700/140729"},{"code":140781,"name":"介休","fullname":"介休市","level":"district","filename":"140000/140700/140781"}]},{"code":140800,"name":"运城","fullname":"运城市","level":"city","filename":"140000/140800","children":[{"code":140802,"name":"盐湖","fullname":"盐湖区","level":"district","filename":"140000/140800/140802"},{"code":140821,"name":"临猗","fullname":"临猗县","level":"district","filename":"140000/140800/140821"},{"code":140822,"name":"万荣","fullname":"万荣县","level":"district","filename":"140000/140800/140822"},{"code":140823,"name":"闻喜","fullname":"闻喜县","level":"district","filename":"140000/140800/140823"},{"code":140824,"name":"稷山","fullname":"稷山县","level":"district","filename":"140000/140800/140824"},{"code":140825,"name":"新绛","fullname":"新绛县","level":"district","filename":"140000/140800/140825"},{"code":140826,"name":"绛县","fullname":"绛县","level":"district","filename":"140000/140800/140826"},{"code":140827,"name":"垣曲","fullname":"垣曲县","level":"district","filename":"140000/140800/140827"},{"code":140828,"name":"夏县","fullname":"夏县","level":"district","filename":"140000/140800/140828"},{"code":140829,"name":"平陆","fullname":"平陆县","level":"district","filename":"140000/140800/140829"},{"code":140830,"name":"芮城","fullname":"芮城县","level":"district","filename":"140000/140800/140830"},{"code":140881,"name":"永济","fullname":"永济市","level":"district","filename":"140000/140800/140881"},{"code":140882,"name":"河津","fullname":"河津市","level":"district","filename":"140000/140800/140882"}]},{"code":140900,"name":"忻州","fullname":"忻州市","level":"city","filename":"140000/140900","children":[{"code":140902,"name":"忻府","fullname":"忻府区","level":"district","filename":"140000/140900/140902"},{"code":140921,"name":"定襄","fullname":"定襄县","level":"district","filename":"140000/140900/140921"},{"code":140922,"name":"五台","fullname":"五台县","level":"district","filename":"140000/140900/140922"},{"code":140923,"name":"代县","fullname":"代县","level":"district","filename":"140000/140900/140923"},{"code":140924,"name":"繁峙","fullname":"繁峙县","level":"district","filename":"140000/140900/140924"},{"code":140925,"name":"宁武","fullname":"宁武县","level":"district","filename":"140000/140900/140925"},{"code":140926,"name":"静乐","fullname":"静乐县","level":"district","filename":"140000/140900/140926"},{"code":140927,"name":"神池","fullname":"神池县","level":"district","filename":"140000/140900/140927"},{"code":140928,"name":"五寨","fullname":"五寨县","level":"district","filename":"140000/140900/140928"},{"code":140929,"name":"岢岚","fullname":"岢岚县","level":"district","filename":"140000/140900/140929"},{"code":140930,"name":"河曲","fullname":"河曲县","level":"district","filename":"140000/140900/140930"},{"code":140931,"name":"保德","fullname":"保德县","level":"district","filename":"140000/140900/140931"},{"code":140932,"name":"偏关","fullname":"偏关县","level":"district","filename":"140000/140900/140932"},{"code":140981,"name":"原平","fullname":"原平市","level":"district","filename":"140000/140900/140981"}]},{"code":141000,"name":"临汾","fullname":"临汾市","level":"city","filename":"140000/141000","children":[{"code":141002,"name":"尧都","fullname":"尧都区","level":"district","filename":"140000/141000/141002"},{"code":141021,"name":"曲沃","fullname":"曲沃县","level":"district","filename":"140000/141000/141021"},{"code":141022,"name":"翼城","fullname":"翼城县","level":"district","filename":"140000/141000/141022"},{"code":141023,"name":"襄汾","fullname":"襄汾县","level":"district","filename":"140000/141000/141023"},{"code":141024,"name":"洪洞","fullname":"洪洞县","level":"district","filename":"140000/141000/141024"},{"code":141025,"name":"古县","fullname":"古县","level":"district","filename":"140000/141000/141025"},{"code":141026,"name":"安泽","fullname":"安泽县","level":"district","filename":"140000/141000/141026"},{"code":141027,"name":"浮山","fullname":"浮山县","level":"district","filename":"140000/141000/141027"},{"code":141028,"name":"吉县","fullname":"吉县","level":"district","filename":"140000/141000/141028"},{"code":141029,"name":"乡宁","fullname":"乡宁县","level":"district","filename":"140000/141000/141029"},{"code":141030,"name":"大宁","fullname":"大宁县","level":"district","filename":"140000/141000/141030"},{"code":141031,"name":"隰县","fullname":"隰县","level":"district","filename":"140000/141000/141031"},{"code":141032,"name":"永和","fullname":"永和县","level":"district","filename":"140000/141000/141032"},{"code":141033,"name":"蒲县","fullname":"蒲县","level":"district","filename":"140000/141000/141033"},{"code":141034,"name":"汾西","fullname":"汾西县","level":"district","filename":"140000/141000/141034"},{"code":141081,"name":"侯马","fullname":"侯马市","level":"district","filename":"140000/141000/141081"},{"code":141082,"name":"霍州","fullname":"霍州市","level":"district","filename":"140000/141000/141082"}]},{"code":141100,"name":"吕梁","fullname":"吕梁市","level":"city","filename":"140000/141100","children":[{"code":141102,"name":"离石","fullname":"离石区","level":"district","filename":"140000/141100/141102"},{"code":141121,"name":"文水","fullname":"文水县","level":"district","filename":"140000/141100/141121"},{"code":141122,"name":"交城","fullname":"交城县","level":"district","filename":"140000/141100/141122"},{"code":141123,"name":"兴县","fullname":"兴县","level":"district","filename":"140000/141100/141123"},{"code":141124,"name":"临县","fullname":"临县","level":"district","filename":"140000/141100/141124"},{"code":141125,"name":"柳林","fullname":"柳林县","level":"district","filename":"140000/141100/141125"},{"code":141126,"name":"石楼","fullname":"石楼县","level":"district","filename":"140000/141100/141126"},{"code":141127,"name":"岚县","fullname":"岚县","level":"district","filename":"140000/141100/141127"},{"code":141128,"name":"方山","fullname":"方山县","level":"district","filename":"140000/141100/141128"},{"code":141129,"name":"中阳","fullname":"中阳县","level":"district","filename":"140000/141100/141129"},{"code":141130,"name":"交口","fullname":"交口县","level":"district","filename":"140000/141100/141130"},{"code":141181,"name":"孝义","fullname":"孝义市","level":"district","filename":"140000/141100/141181"},{"code":141182,"name":"汾阳","fullname":"汾阳市","level":"district","filename":"140000/141100/141182"}]}]},{"code":150000,"name":"内蒙古","fullname":"内蒙古自治区","level":"province","filename":"150000","children":[{"code":150100,"name":"呼和浩特","fullname":"呼和浩特市","level":"city","filename":"150000/150100","children":[{"code":150102,"name":"新城","fullname":"新城区","level":"district","filename":"150000/150100/150102"},{"code":150103,"name":"回民","fullname":"回民区","level":"district","filename":"150000/150100/150103"},{"code":150104,"name":"玉泉","fullname":"玉泉区","level":"district","filename":"150000/150100/150104"},{"code":150105,"name":"赛罕","fullname":"赛罕区","level":"district","filename":"150000/150100/150105"},{"code":150121,"name":"土默特左","fullname":"土默特左旗","level":"district","filename":"150000/150100/150121"},{"code":150122,"name":"托克托","fullname":"托克托县","level":"district","filename":"150000/150100/150122"},{"code":150123,"name":"和林格尔","fullname":"和林格尔县","level":"district","filename":"150000/150100/150123"},{"code":150124,"name":"清水河","fullname":"清水河县","level":"district","filename":"150000/150100/150124"},{"code":150125,"name":"武川","fullname":"武川县","level":"district","filename":"150000/150100/150125"}]},{"code":150200,"name":"包头","fullname":"包头市","level":"city","filename":"150000/150200","children":[{"code":150202,"name":"东河","fullname":"东河区","level":"district","filename":"150000/150200/150202"},{"code":150203,"name":"昆都仑","fullname":"昆都仑区","level":"district","filename":"150000/150200/150203"},{"code":150204,"name":"青山","fullname":"青山区","level":"district","filename":"150000/150200/150204"},{"code":150205,"name":"石拐","fullname":"石拐区","level":"district","filename":"150000/150200/150205"},{"code":150206,"name":"白云鄂博","fullname":"白云鄂博矿区","level":"district","filename":"150000/150200/150206"},{"code":150207,"name":"九原","fullname":"九原区","level":"district","filename":"150000/150200/150207"},{"code":150221,"name":"土默特右","fullname":"土默特右旗","level":"district","filename":"150000/150200/150221"},{"code":150222,"name":"固阳","fullname":"固阳县","level":"district","filename":"150000/150200/150222"},{"code":150223,"name":"达尔罕茂明安","fullname":"达尔罕茂明安联合旗","level":"district","filename":"150000/150200/150223"}]},{"code":150300,"name":"乌海","fullname":"乌海市","level":"city","filename":"150000/150300","children":[{"code":150302,"name":"海勃湾","fullname":"海勃湾区","level":"district","filename":"150000/150300/150302"},{"code":150303,"name":"海南","fullname":"海南区","level":"district","filename":"150000/150300/150303"},{"code":150304,"name":"乌达","fullname":"乌达区","level":"district","filename":"150000/150300/150304"}]},{"code":150400,"name":"赤峰","fullname":"赤峰市","level":"city","filename":"150000/150400","children":[{"code":150402,"name":"红山","fullname":"红山区","level":"district","filename":"150000/150400/150402"},{"code":150403,"name":"元宝山","fullname":"元宝山区","level":"district","filename":"150000/150400/150403"},{"code":150404,"name":"松山","fullname":"松山区","level":"district","filename":"150000/150400/150404"},{"code":150421,"name":"阿鲁科尔沁","fullname":"阿鲁科尔沁旗","level":"district","filename":"150000/150400/150421"},{"code":150422,"name":"巴林左","fullname":"巴林左旗","level":"district","filename":"150000/150400/150422"},{"code":150423,"name":"巴林右","fullname":"巴林右旗","level":"district","filename":"150000/150400/150423"},{"code":150424,"name":"林西","fullname":"林西县","level":"district","filename":"150000/150400/150424"},{"code":150425,"name":"克什克腾","fullname":"克什克腾旗","level":"district","filename":"150000/150400/150425"},{"code":150426,"name":"翁牛特","fullname":"翁牛特旗","level":"district","filename":"150000/150400/150426"},{"code":150428,"name":"喀喇沁","fullname":"喀喇沁旗","level":"district","filename":"150000/150400/150428"},{"code":150429,"name":"宁城","fullname":"宁城县","level":"district","filename":"150000/150400/150429"},{"code":150430,"name":"敖汉","fullname":"敖汉旗","level":"district","filename":"150000/150400/150430"}]},{"code":150500,"name":"通辽","fullname":"通辽市","level":"city","filename":"150000/150500","children":[{"code":150502,"name":"科尔沁","fullname":"科尔沁区","level":"district","filename":"150000/150500/150502"},{"code":150521,"name":"科尔沁左翼中","fullname":"科尔沁左翼中旗","level":"district","filename":"150000/150500/150521"},{"code":150522,"name":"科尔沁左翼后","fullname":"科尔沁左翼后旗","level":"district","filename":"150000/150500/150522"},{"code":150523,"name":"开鲁","fullname":"开鲁县","level":"district","filename":"150000/150500/150523"},{"code":150524,"name":"库伦","fullname":"库伦旗","level":"district","filename":"150000/150500/150524"},{"code":150525,"name":"奈曼","fullname":"奈曼旗","level":"district","filename":"150000/150500/150525"},{"code":150526,"name":"扎鲁特","fullname":"扎鲁特旗","level":"district","filename":"150000/150500/150526"},{"code":150581,"name":"霍林郭勒","fullname":"霍林郭勒市","level":"district","filename":"150000/150500/150581"}]},{"code":150600,"name":"鄂尔多斯","fullname":"鄂尔多斯市","level":"city","filename":"150000/150600","children":[{"code":150602,"name":"东胜","fullname":"东胜区","level":"district","filename":"150000/150600/150602"},{"code":150603,"name":"康巴什","fullname":"康巴什区","level":"district","filename":"150000/150600/150603"},{"code":150621,"name":"达拉特","fullname":"达拉特旗","level":"district","filename":"150000/150600/150621"},{"code":150622,"name":"准格尔","fullname":"准格尔旗","level":"district","filename":"150000/150600/150622"},{"code":150623,"name":"鄂托克前","fullname":"鄂托克前旗","level":"district","filename":"150000/150600/150623"},{"code":150624,"name":"鄂托克","fullname":"鄂托克旗","level":"district","filename":"150000/150600/150624"},{"code":150625,"name":"杭锦","fullname":"杭锦旗","level":"district","filename":"150000/150600/150625"},{"code":150626,"name":"乌审","fullname":"乌审旗","level":"district","filename":"150000/150600/150626"},{"code":150627,"name":"伊金霍洛","fullname":"伊金霍洛旗","level":"district","filename":"150000/150600/150627"}]},{"code":150700,"name":"呼伦贝尔","fullname":"呼伦贝尔市","level":"city","filename":"150000/150700","children":[{"code":150702,"name":"海拉尔","fullname":"海拉尔区","level":"district","filename":"150000/150700/150702"},{"code":150703,"name":"扎赉诺尔","fullname":"扎赉诺尔区","level":"district","filename":"150000/150700/150703"},{"code":150721,"name":"阿荣","fullname":"阿荣旗","level":"district","filename":"150000/150700/150721"},{"code":150722,"name":"莫力达瓦","fullname":"莫力达瓦达斡尔族自治旗","level":"district","filename":"150000/150700/150722"},{"code":150723,"name":"鄂伦春","fullname":"鄂伦春自治旗","level":"district","filename":"150000/150700/150723"},{"code":150724,"name":"鄂温克族","fullname":"鄂温克族自治旗","level":"district","filename":"150000/150700/150724"},{"code":150725,"name":"陈巴尔虎","fullname":"陈巴尔虎旗","level":"district","filename":"150000/150700/150725"},{"code":150726,"name":"新巴尔虎左","fullname":"新巴尔虎左旗","level":"district","filename":"150000/150700/150726"},{"code":150727,"name":"新巴尔虎右","fullname":"新巴尔虎右旗","level":"district","filename":"150000/150700/150727"},{"code":150781,"name":"满洲里","fullname":"满洲里市","level":"district","filename":"150000/150700/150781"},{"code":150782,"name":"牙克石","fullname":"牙克石市","level":"district","filename":"150000/150700/150782"},{"code":150783,"name":"扎兰屯","fullname":"扎兰屯市","level":"district","filename":"150000/150700/150783"},{"code":150784,"name":"额尔古纳","fullname":"额尔古纳市","level":"district","filename":"150000/150700/150784"},{"code":150785,"name":"根河","fullname":"根河市","level":"district","filename":"150000/150700/150785"}]},{"code":150800,"name":"巴彦淖尔","fullname":"巴彦淖尔市","level":"city","filename":"150000/150800","children":[{"code":150802,"name":"临河","fullname":"临河区","level":"district","filename":"150000/150800/150802"},{"code":150821,"name":"五原","fullname":"五原县","level":"district","filename":"150000/150800/150821"},{"code":150822,"name":"磴口","fullname":"磴口县","level":"district","filename":"150000/150800/150822"},{"code":150823,"name":"乌拉特前","fullname":"乌拉特前旗","level":"district","filename":"150000/150800/150823"},{"code":150824,"name":"乌拉特中","fullname":"乌拉特中旗","level":"district","filename":"150000/150800/150824"},{"code":150825,"name":"乌拉特后","fullname":"乌拉特后旗","level":"district","filename":"150000/150800/150825"},{"code":150826,"name":"杭锦后","fullname":"杭锦后旗","level":"district","filename":"150000/150800/150826"}]},{"code":150900,"name":"乌兰察布","fullname":"乌兰察布市","level":"city","filename":"150000/150900","children":[{"code":150902,"name":"集宁","fullname":"集宁区","level":"district","filename":"150000/150900/150902"},{"code":150921,"name":"卓资","fullname":"卓资县","level":"district","filename":"150000/150900/150921"},{"code":150922,"name":"化德","fullname":"化德县","level":"district","filename":"150000/150900/150922"},{"code":150923,"name":"商都","fullname":"商都县","level":"district","filename":"150000/150900/150923"},{"code":150924,"name":"兴和","fullname":"兴和县","level":"district","filename":"150000/150900/150924"},{"code":150925,"name":"凉城","fullname":"凉城县","level":"district","filename":"150000/150900/150925"},{"code":150926,"name":"察哈尔右翼前","fullname":"察哈尔右翼前旗","level":"district","filename":"150000/150900/150926"},{"code":150927,"name":"察哈尔右翼中","fullname":"察哈尔右翼中旗","level":"district","filename":"150000/150900/150927"},{"code":150928,"name":"察哈尔右翼后","fullname":"察哈尔右翼后旗","level":"district","filename":"150000/150900/150928"},{"code":150929,"name":"四子王","fullname":"四子王旗","level":"district","filename":"150000/150900/150929"},{"code":150981,"name":"丰镇","fullname":"丰镇市","level":"district","filename":"150000/150900/150981"}]},{"code":152200,"name":"兴安","fullname":"兴安盟","level":"city","filename":"150000/152200","children":[{"code":152201,"name":"乌兰浩特","fullname":"乌兰浩特市","level":"district","filename":"150000/152200/152201"},{"code":152202,"name":"阿尔山","fullname":"阿尔山市","level":"district","filename":"150000/152200/152202"},{"code":152221,"name":"科尔沁右翼前","fullname":"科尔沁右翼前旗","level":"district","filename":"150000/152200/152221"},{"code":152222,"name":"科尔沁右翼中","fullname":"科尔沁右翼中旗","level":"district","filename":"150000/152200/152222"},{"code":152223,"name":"扎赉特","fullname":"扎赉特旗","level":"district","filename":"150000/152200/152223"},{"code":152224,"name":"突泉","fullname":"突泉县","level":"district","filename":"150000/152200/152224"}]},{"code":152500,"name":"锡林郭勒","fullname":"锡林郭勒盟","level":"city","filename":"150000/152500","children":[{"code":152501,"name":"二连浩特","fullname":"二连浩特市","level":"district","filename":"150000/152500/152501"},{"code":152502,"name":"锡林浩特","fullname":"锡林浩特市","level":"district","filename":"150000/152500/152502"},{"code":152522,"name":"阿巴嘎","fullname":"阿巴嘎旗","level":"district","filename":"150000/152500/152522"},{"code":152523,"name":"苏尼特左","fullname":"苏尼特左旗","level":"district","filename":"150000/152500/152523"},{"code":152524,"name":"苏尼特右","fullname":"苏尼特右旗","level":"district","filename":"150000/152500/152524"},{"code":152525,"name":"东乌珠穆沁","fullname":"东乌珠穆沁旗","level":"district","filename":"150000/152500/152525"},{"code":152526,"name":"西乌珠穆沁","fullname":"西乌珠穆沁旗","level":"district","filename":"150000/152500/152526"},{"code":152527,"name":"太仆寺","fullname":"太仆寺旗","level":"district","filename":"150000/152500/152527"},{"code":152528,"name":"镶黄","fullname":"镶黄旗","level":"district","filename":"150000/152500/152528"},{"code":152529,"name":"正镶白","fullname":"正镶白旗","level":"district","filename":"150000/152500/152529"},{"code":152530,"name":"正蓝","fullname":"正蓝旗","level":"district","filename":"150000/152500/152530"},{"code":152531,"name":"多伦","fullname":"多伦县","level":"district","filename":"150000/152500/152531"}]},{"code":152900,"name":"阿拉善","fullname":"阿拉善盟","level":"city","filename":"150000/152900","children":[{"code":152921,"name":"阿拉善左","fullname":"阿拉善左旗","level":"district","filename":"150000/152900/152921"},{"code":152922,"name":"阿拉善右","fullname":"阿拉善右旗","level":"district","filename":"150000/152900/152922"},{"code":152923,"name":"额济纳","fullname":"额济纳旗","level":"district","filename":"150000/152900/152923"}]}]},{"code":210000,"name":"辽宁","fullname":"辽宁省","level":"province","filename":"210000","children":[{"code":210100,"name":"沈阳","fullname":"沈阳市","level":"city","filename":"210000/210100","children":[{"code":210102,"name":"和平","fullname":"和平区","level":"district","filename":"210000/210100/210102"},{"code":210103,"name":"沈河","fullname":"沈河区","level":"district","filename":"210000/210100/210103"},{"code":210104,"name":"大东","fullname":"大东区","level":"district","filename":"210000/210100/210104"},{"code":210105,"name":"皇姑","fullname":"皇姑区","level":"district","filename":"210000/210100/210105"},{"code":210106,"name":"铁西","fullname":"铁西区","level":"district","filename":"210000/210100/210106"},{"code":210111,"name":"苏家屯","fullname":"苏家屯区","level":"district","filename":"210000/210100/210111"},{"code":210112,"name":"浑南","fullname":"浑南区","level":"district","filename":"210000/210100/210112"},{"code":210113,"name":"沈北","fullname":"沈北新区","level":"district","filename":"210000/210100/210113"},{"code":210114,"name":"于洪","fullname":"于洪区","level":"district","filename":"210000/210100/210114"},{"code":210115,"name":"辽中","fullname":"辽中区","level":"district","filename":"210000/210100/210115"},{"code":210123,"name":"康平","fullname":"康平县","level":"district","filename":"210000/210100/210123"},{"code":210124,"name":"法库","fullname":"法库县","level":"district","filename":"210000/210100/210124"},{"code":210181,"name":"新民","fullname":"新民市","level":"district","filename":"210000/210100/210181"}]},{"code":210200,"name":"大连","fullname":"大连市","level":"city","filename":"210000/210200","children":[{"code":210202,"name":"中山","fullname":"中山区","level":"district","filename":"210000/210200/210202"},{"code":210203,"name":"西岗","fullname":"西岗区","level":"district","filename":"210000/210200/210203"},{"code":210204,"name":"沙河口","fullname":"沙河口区","level":"district","filename":"210000/210200/210204"},{"code":210211,"name":"甘井子","fullname":"甘井子区","level":"district","filename":"210000/210200/210211"},{"code":210212,"name":"旅顺口","fullname":"旅顺口区","level":"district","filename":"210000/210200/210212"},{"code":210213,"name":"金州","fullname":"金州区","level":"district","filename":"210000/210200/210213"},{"code":210214,"name":"普兰店","fullname":"普兰店区","level":"district","filename":"210000/210200/210214"},{"code":210224,"name":"长海","fullname":"长海县","level":"district","filename":"210000/210200/210224"},{"code":210281,"name":"瓦房店","fullname":"瓦房店市","level":"district","filename":"210000/210200/210281"},{"code":210283,"name":"庄河","fullname":"庄河市","level":"district","filename":"210000/210200/210283"}]},{"code":210300,"name":"鞍山","fullname":"鞍山市","level":"city","filename":"210000/210300","children":[{"code":210302,"name":"铁东","fullname":"铁东区","level":"district","filename":"210000/210300/210302"},{"code":210303,"name":"铁西","fullname":"铁西区","level":"district","filename":"210000/210300/210303"},{"code":210304,"name":"立山","fullname":"立山区","level":"district","filename":"210000/210300/210304"},{"code":210311,"name":"千山","fullname":"千山区","level":"district","filename":"210000/210300/210311"},{"code":210321,"name":"台安","fullname":"台安县","level":"district","filename":"210000/210300/210321"},{"code":210323,"name":"岫岩","fullname":"岫岩满族自治县","level":"district","filename":"210000/210300/210323"},{"code":210381,"name":"海城","fullname":"海城市","level":"district","filename":"210000/210300/210381"}]},{"code":210400,"name":"抚顺","fullname":"抚顺市","level":"city","filename":"210000/210400","children":[{"code":210402,"name":"新抚","fullname":"新抚区","level":"district","filename":"210000/210400/210402"},{"code":210403,"name":"东洲","fullname":"东洲区","level":"district","filename":"210000/210400/210403"},{"code":210404,"name":"望花","fullname":"望花区","level":"district","filename":"210000/210400/210404"},{"code":210411,"name":"顺城","fullname":"顺城区","level":"district","filename":"210000/210400/210411"},{"code":210421,"name":"抚顺","fullname":"抚顺县","level":"district","filename":"210000/210400/210421"},{"code":210422,"name":"新宾","fullname":"新宾满族自治县","level":"district","filename":"210000/210400/210422"},{"code":210423,"name":"清原","fullname":"清原满族自治县","level":"district","filename":"210000/210400/210423"}]},{"code":210500,"name":"本溪","fullname":"本溪市","level":"city","filename":"210000/210500","children":[{"code":210502,"name":"平山","fullname":"平山区","level":"district","filename":"210000/210500/210502"},{"code":210503,"name":"溪湖","fullname":"溪湖区","level":"district","filename":"210000/210500/210503"},{"code":210504,"name":"明山","fullname":"明山区","level":"district","filename":"210000/210500/210504"},{"code":210505,"name":"南芬","fullname":"南芬区","level":"district","filename":"210000/210500/210505"},{"code":210521,"name":"本溪","fullname":"本溪满族自治县","level":"district","filename":"210000/210500/210521"},{"code":210522,"name":"桓仁","fullname":"桓仁满族自治县","level":"district","filename":"210000/210500/210522"}]},{"code":210600,"name":"丹东","fullname":"丹东市","level":"city","filename":"210000/210600","children":[{"code":210602,"name":"元宝","fullname":"元宝区","level":"district","filename":"210000/210600/210602"},{"code":210603,"name":"振兴","fullname":"振兴区","level":"district","filename":"210000/210600/210603"},{"code":210604,"name":"振安","fullname":"振安区","level":"district","filename":"210000/210600/210604"},{"code":210624,"name":"宽甸","fullname":"宽甸满族自治县","level":"district","filename":"210000/210600/210624"},{"code":210681,"name":"东港","fullname":"东港市","level":"district","filename":"210000/210600/210681"},{"code":210682,"name":"凤城","fullname":"凤城市","level":"district","filename":"210000/210600/210682"}]},{"code":210700,"name":"锦州","fullname":"锦州市","level":"city","filename":"210000/210700","children":[{"code":210702,"name":"古塔","fullname":"古塔区","level":"district","filename":"210000/210700/210702"},{"code":210703,"name":"凌河","fullname":"凌河区","level":"district","filename":"210000/210700/210703"},{"code":210711,"name":"太和","fullname":"太和区","level":"district","filename":"210000/210700/210711"},{"code":210726,"name":"黑山","fullname":"黑山县","level":"district","filename":"210000/210700/210726"},{"code":210727,"name":"义县","fullname":"义县","level":"district","filename":"210000/210700/210727"},{"code":210781,"name":"凌海","fullname":"凌海市","level":"district","filename":"210000/210700/210781"},{"code":210782,"name":"北镇","fullname":"北镇市","level":"district","filename":"210000/210700/210782"}]},{"code":210800,"name":"营口","fullname":"营口市","level":"city","filename":"210000/210800","children":[{"code":210802,"name":"站前","fullname":"站前区","level":"district","filename":"210000/210800/210802"},{"code":210803,"name":"西市区","fullname":"西市区","level":"district","filename":"210000/210800/210803"},{"code":210804,"name":"鲅鱼圈","fullname":"鲅鱼圈区","level":"district","filename":"210000/210800/210804"},{"code":210811,"name":"老边","fullname":"老边区","level":"district","filename":"210000/210800/210811"},{"code":210881,"name":"盖州","fullname":"盖州市","level":"district","filename":"210000/210800/210881"},{"code":210882,"name":"大石桥","fullname":"大石桥市","level":"district","filename":"210000/210800/210882"}]},{"code":210900,"name":"阜新","fullname":"阜新市","level":"city","filename":"210000/210900","children":[{"code":210902,"name":"海州","fullname":"海州区","level":"district","filename":"210000/210900/210902"},{"code":210903,"name":"新邱","fullname":"新邱区","level":"district","filename":"210000/210900/210903"},{"code":210904,"name":"太平","fullname":"太平区","level":"district","filename":"210000/210900/210904"},{"code":210905,"name":"清河门","fullname":"清河门区","level":"district","filename":"210000/210900/210905"},{"code":210911,"name":"细河","fullname":"细河区","level":"district","filename":"210000/210900/210911"},{"code":210921,"name":"阜新","fullname":"阜新蒙古族自治县","level":"district","filename":"210000/210900/210921"},{"code":210922,"name":"彰武","fullname":"彰武县","level":"district","filename":"210000/210900/210922"}]},{"code":211000,"name":"辽阳","fullname":"辽阳市","level":"city","filename":"210000/211000","children":[{"code":211002,"name":"白塔","fullname":"白塔区","level":"district","filename":"210000/211000/211002"},{"code":211003,"name":"文圣","fullname":"文圣区","level":"district","filename":"210000/211000/211003"},{"code":211004,"name":"宏伟","fullname":"宏伟区","level":"district","filename":"210000/211000/211004"},{"code":211005,"name":"弓长岭","fullname":"弓长岭区","level":"district","filename":"210000/211000/211005"},{"code":211011,"name":"太子河","fullname":"太子河区","level":"district","filename":"210000/211000/211011"},{"code":211021,"name":"辽阳","fullname":"辽阳县","level":"district","filename":"210000/211000/211021"},{"code":211081,"name":"灯塔","fullname":"灯塔市","level":"district","filename":"210000/211000/211081"}]},{"code":211100,"name":"盘锦","fullname":"盘锦市","level":"city","filename":"210000/211100","children":[{"code":211102,"name":"双台子","fullname":"双台子区","level":"district","filename":"210000/211100/211102"},{"code":211103,"name":"兴隆台","fullname":"兴隆台区","level":"district","filename":"210000/211100/211103"},{"code":211104,"name":"大洼","fullname":"大洼区","level":"district","filename":"210000/211100/211104"},{"code":211122,"name":"盘山","fullname":"盘山县","level":"district","filename":"210000/211100/211122"}]},{"code":211200,"name":"铁岭","fullname":"铁岭市","level":"city","filename":"210000/211200","children":[{"code":211202,"name":"银州","fullname":"银州区","level":"district","filename":"210000/211200/211202"},{"code":211204,"name":"清河","fullname":"清河区","level":"district","filename":"210000/211200/211204"},{"code":211221,"name":"铁岭","fullname":"铁岭县","level":"district","filename":"210000/211200/211221"},{"code":211223,"name":"西丰","fullname":"西丰县","level":"district","filename":"210000/211200/211223"},{"code":211224,"name":"昌图","fullname":"昌图县","level":"district","filename":"210000/211200/211224"},{"code":211281,"name":"调兵山","fullname":"调兵山市","level":"district","filename":"210000/211200/211281"},{"code":211282,"name":"开原","fullname":"开原市","level":"district","filename":"210000/211200/211282"}]},{"code":211300,"name":"朝阳","fullname":"朝阳市","level":"city","filename":"210000/211300","children":[{"code":211302,"name":"双塔","fullname":"双塔区","level":"district","filename":"210000/211300/211302"},{"code":211303,"name":"龙城","fullname":"龙城区","level":"district","filename":"210000/211300/211303"},{"code":211321,"name":"朝阳","fullname":"朝阳县","level":"district","filename":"210000/211300/211321"},{"code":211322,"name":"建平","fullname":"建平县","level":"district","filename":"210000/211300/211322"},{"code":211324,"name":"喀喇沁左翼","fullname":"喀喇沁左翼蒙古族自治县","level":"district","filename":"210000/211300/211324"},{"code":211381,"name":"北票","fullname":"北票市","level":"district","filename":"210000/211300/211381"},{"code":211382,"name":"凌源","fullname":"凌源市","level":"district","filename":"210000/211300/211382"}]},{"code":211400,"name":"葫芦岛","fullname":"葫芦岛市","level":"city","filename":"210000/211400","children":[{"code":211402,"name":"连山","fullname":"连山区","level":"district","filename":"210000/211400/211402"},{"code":211403,"name":"龙港","fullname":"龙港区","level":"district","filename":"210000/211400/211403"},{"code":211404,"name":"南票","fullname":"南票区","level":"district","filename":"210000/211400/211404"},{"code":211421,"name":"绥中","fullname":"绥中县","level":"district","filename":"210000/211400/211421"},{"code":211422,"name":"建昌","fullname":"建昌县","level":"district","filename":"210000/211400/211422"},{"code":211481,"name":"兴城","fullname":"兴城市","level":"district","filename":"210000/211400/211481"}]}]},{"code":220000,"name":"吉林","fullname":"吉林省","level":"province","filename":"220000","children":[{"code":220100,"name":"长春","fullname":"长春市","level":"city","filename":"220000/220100","children":[{"code":220102,"name":"南关","fullname":"南关区","level":"district","filename":"220000/220100/220102"},{"code":220103,"name":"宽城","fullname":"宽城区","level":"district","filename":"220000/220100/220103"},{"code":220104,"name":"朝阳","fullname":"朝阳区","level":"district","filename":"220000/220100/220104"},{"code":220105,"name":"二道","fullname":"二道区","level":"district","filename":"220000/220100/220105"},{"code":220106,"name":"绿园","fullname":"绿园区","level":"district","filename":"220000/220100/220106"},{"code":220112,"name":"双阳","fullname":"双阳区","level":"district","filename":"220000/220100/220112"},{"code":220113,"name":"九台","fullname":"九台区","level":"district","filename":"220000/220100/220113"},{"code":220122,"name":"农安","fullname":"农安县","level":"district","filename":"220000/220100/220122"},{"code":220182,"name":"榆树","fullname":"榆树市","level":"district","filename":"220000/220100/220182"},{"code":220183,"name":"德惠","fullname":"德惠市","level":"district","filename":"220000/220100/220183"},{"code":220184,"name":"公主岭","fullname":"公主岭市","level":"district","filename":"220000/220100/220184"}]},{"code":220200,"name":"吉林","fullname":"吉林市","level":"city","filename":"220000/220200","children":[{"code":220202,"name":"昌邑","fullname":"昌邑区","level":"district","filename":"220000/220200/220202"},{"code":220203,"name":"龙潭","fullname":"龙潭区","level":"district","filename":"220000/220200/220203"},{"code":220204,"name":"船营","fullname":"船营区","level":"district","filename":"220000/220200/220204"},{"code":220211,"name":"丰满","fullname":"丰满区","level":"district","filename":"220000/220200/220211"},{"code":220221,"name":"永吉","fullname":"永吉县","level":"district","filename":"220000/220200/220221"},{"code":220281,"name":"蛟河","fullname":"蛟河市","level":"district","filename":"220000/220200/220281"},{"code":220282,"name":"桦甸","fullname":"桦甸市","level":"district","filename":"220000/220200/220282"},{"code":220283,"name":"舒兰","fullname":"舒兰市","level":"district","filename":"220000/220200/220283"},{"code":220284,"name":"磐石","fullname":"磐石市","level":"district","filename":"220000/220200/220284"}]},{"code":220300,"name":"四平","fullname":"四平市","level":"city","filename":"220000/220300","children":[{"code":220302,"name":"铁西","fullname":"铁西区","level":"district","filename":"220000/220300/220302"},{"code":220303,"name":"铁东","fullname":"铁东区","level":"district","filename":"220000/220300/220303"},{"code":220322,"name":"梨树","fullname":"梨树县","level":"district","filename":"220000/220300/220322"},{"code":220323,"name":"伊通","fullname":"伊通满族自治县","level":"district","filename":"220000/220300/220323"},{"code":220382,"name":"双辽","fullname":"双辽市","level":"district","filename":"220000/220300/220382"}]},{"code":220400,"name":"辽源","fullname":"辽源市","level":"city","filename":"220000/220400","children":[{"code":220402,"name":"龙山","fullname":"龙山区","level":"district","filename":"220000/220400/220402"},{"code":220403,"name":"西安","fullname":"西安区","level":"district","filename":"220000/220400/220403"},{"code":220421,"name":"东丰","fullname":"东丰县","level":"district","filename":"220000/220400/220421"},{"code":220422,"name":"东辽","fullname":"东辽县","level":"district","filename":"220000/220400/220422"}]},{"code":220500,"name":"通化","fullname":"通化市","level":"city","filename":"220000/220500","children":[{"code":220502,"name":"东昌","fullname":"东昌区","level":"district","filename":"220000/220500/220502"},{"code":220503,"name":"二道江","fullname":"二道江区","level":"district","filename":"220000/220500/220503"},{"code":220521,"name":"通化","fullname":"通化县","level":"district","filename":"220000/220500/220521"},{"code":220523,"name":"辉南","fullname":"辉南县","level":"district","filename":"220000/220500/220523"},{"code":220524,"name":"柳河","fullname":"柳河县","level":"district","filename":"220000/220500/220524"},{"code":220581,"name":"梅河口","fullname":"梅河口市","level":"district","filename":"220000/220500/220581"},{"code":220582,"name":"集安","fullname":"集安市","level":"district","filename":"220000/220500/220582"}]},{"code":220600,"name":"白山","fullname":"白山市","level":"city","filename":"220000/220600","children":[{"code":220602,"name":"浑江","fullname":"浑江区","level":"district","filename":"220000/220600/220602"},{"code":220605,"name":"江源","fullname":"江源区","level":"district","filename":"220000/220600/220605"},{"code":220621,"name":"抚松","fullname":"抚松县","level":"district","filename":"220000/220600/220621"},{"code":220622,"name":"靖宇","fullname":"靖宇县","level":"district","filename":"220000/220600/220622"},{"code":220623,"name":"长白","fullname":"长白朝鲜族自治县","level":"district","filename":"220000/220600/220623"},{"code":220681,"name":"临江","fullname":"临江市","level":"district","filename":"220000/220600/220681"}]},{"code":220700,"name":"松原","fullname":"松原市","level":"city","filename":"220000/220700","children":[{"code":220702,"name":"宁江","fullname":"宁江区","level":"district","filename":"220000/220700/220702"},{"code":220721,"name":"前郭尔罗斯","fullname":"前郭尔罗斯蒙古族自治县","level":"district","filename":"220000/220700/220721"},{"code":220722,"name":"长岭","fullname":"长岭县","level":"district","filename":"220000/220700/220722"},{"code":220723,"name":"乾安","fullname":"乾安县","level":"district","filename":"220000/220700/220723"},{"code":220781,"name":"扶余","fullname":"扶余市","level":"district","filename":"220000/220700/220781"}]},{"code":220800,"name":"白城","fullname":"白城市","level":"city","filename":"220000/220800","children":[{"code":220802,"name":"洮北","fullname":"洮北区","level":"district","filename":"220000/220800/220802"},{"code":220821,"name":"镇赉","fullname":"镇赉县","level":"district","filename":"220000/220800/220821"},{"code":220822,"name":"通榆","fullname":"通榆县","level":"district","filename":"220000/220800/220822"},{"code":220881,"name":"洮南","fullname":"洮南市","level":"district","filename":"220000/220800/220881"},{"code":220882,"name":"大安","fullname":"大安市","level":"district","filename":"220000/220800/220882"}]},{"code":222400,"name":"延边","fullname":"延边朝鲜族自治州","level":"city","filename":"220000/222400","children":[{"code":222401,"name":"延吉","fullname":"延吉市","level":"district","filename":"220000/222400/222401"},{"code":222402,"name":"图们","fullname":"图们市","level":"district","filename":"220000/222400/222402"},{"code":222403,"name":"敦化","fullname":"敦化市","level":"district","filename":"220000/222400/222403"},{"code":222404,"name":"珲春","fullname":"珲春市","level":"district","filename":"220000/222400/222404"},{"code":222405,"name":"龙井","fullname":"龙井市","level":"district","filename":"220000/222400/222405"},{"code":222406,"name":"和龙","fullname":"和龙市","level":"district","filename":"220000/222400/222406"},{"code":222424,"name":"汪清","fullname":"汪清县","level":"district","filename":"220000/222400/222424"},{"code":222426,"name":"安图","fullname":"安图县","level":"district","filename":"220000/222400/222426"}]}]},{"code":230000,"name":"黑龙江","fullname":"黑龙江省","level":"province","filename":"230000","children":[{"code":230100,"name":"哈尔滨","fullname":"哈尔滨市","level":"city","filename":"230000/230100","children":[{"code":230102,"name":"道里","fullname":"道里区","level":"district","filename":"230000/230100/230102"},{"code":230103,"name":"南岗","fullname":"南岗区","level":"district","filename":"230000/230100/230103"},{"code":230104,"name":"道外","fullname":"道外区","level":"district","filename":"230000/230100/230104"},{"code":230108,"name":"平房","fullname":"平房区","level":"district","filename":"230000/230100/230108"},{"code":230109,"name":"松北","fullname":"松北区","level":"district","filename":"230000/230100/230109"},{"code":230110,"name":"香坊","fullname":"香坊区","level":"district","filename":"230000/230100/230110"},{"code":230111,"name":"呼兰","fullname":"呼兰区","level":"district","filename":"230000/230100/230111"},{"code":230112,"name":"阿城","fullname":"阿城区","level":"district","filename":"230000/230100/230112"},{"code":230113,"name":"双城","fullname":"双城区","level":"district","filename":"230000/230100/230113"},{"code":230123,"name":"依兰","fullname":"依兰县","level":"district","filename":"230000/230100/230123"},{"code":230124,"name":"方正","fullname":"方正县","level":"district","filename":"230000/230100/230124"},{"code":230125,"name":"宾县","fullname":"宾县","level":"district","filename":"230000/230100/230125"},{"code":230126,"name":"巴彦","fullname":"巴彦县","level":"district","filename":"230000/230100/230126"},{"code":230127,"name":"木兰","fullname":"木兰县","level":"district","filename":"230000/230100/230127"},{"code":230128,"name":"通河","fullname":"通河县","level":"district","filename":"230000/230100/230128"},{"code":230129,"name":"延寿","fullname":"延寿县","level":"district","filename":"230000/230100/230129"},{"code":230183,"name":"尚志","fullname":"尚志市","level":"district","filename":"230000/230100/230183"},{"code":230184,"name":"五常","fullname":"五常市","level":"district","filename":"230000/230100/230184"}]},{"code":230200,"name":"齐齐哈尔","fullname":"齐齐哈尔市","level":"city","filename":"230000/230200","children":[{"code":230202,"name":"龙沙","fullname":"龙沙区","level":"district","filename":"230000/230200/230202"},{"code":230203,"name":"建华","fullname":"建华区","level":"district","filename":"230000/230200/230203"},{"code":230204,"name":"铁锋","fullname":"铁锋区","level":"district","filename":"230000/230200/230204"},{"code":230205,"name":"昂昂溪","fullname":"昂昂溪区","level":"district","filename":"230000/230200/230205"},{"code":230206,"name":"富拉尔基","fullname":"富拉尔基区","level":"district","filename":"230000/230200/230206"},{"code":230207,"name":"碾子山","fullname":"碾子山区","level":"district","filename":"230000/230200/230207"},{"code":230208,"name":"梅里斯","fullname":"梅里斯达斡尔族区","level":"district","filename":"230000/230200/230208"},{"code":230221,"name":"龙江","fullname":"龙江县","level":"district","filename":"230000/230200/230221"},{"code":230223,"name":"依安","fullname":"依安县","level":"district","filename":"230000/230200/230223"},{"code":230224,"name":"泰来","fullname":"泰来县","level":"district","filename":"230000/230200/230224"},{"code":230225,"name":"甘南","fullname":"甘南县","level":"district","filename":"230000/230200/230225"},{"code":230227,"name":"富裕","fullname":"富裕县","level":"district","filename":"230000/230200/230227"},{"code":230229,"name":"克山","fullname":"克山县","level":"district","filename":"230000/230200/230229"},{"code":230230,"name":"克东","fullname":"克东县","level":"district","filename":"230000/230200/230230"},{"code":230231,"name":"拜泉","fullname":"拜泉县","level":"district","filename":"230000/230200/230231"},{"code":230281,"name":"讷河","fullname":"讷河市","level":"district","filename":"230000/230200/230281"}]},{"code":230300,"name":"鸡西","fullname":"鸡西市","level":"city","filename":"230000/230300","children":[{"code":230302,"name":"鸡冠","fullname":"鸡冠区","level":"district","filename":"230000/230300/230302"},{"code":230303,"name":"恒山","fullname":"恒山区","level":"district","filename":"230000/230300/230303"},{"code":230304,"name":"滴道","fullname":"滴道区","level":"district","filename":"230000/230300/230304"},{"code":230305,"name":"梨树","fullname":"梨树区","level":"district","filename":"230000/230300/230305"},{"code":230306,"name":"城子河","fullname":"城子河区","level":"district","filename":"230000/230300/230306"},{"code":230307,"name":"麻山","fullname":"麻山区","level":"district","filename":"230000/230300/230307"},{"code":230321,"name":"鸡东","fullname":"鸡东县","level":"district","filename":"230000/230300/230321"},{"code":230381,"name":"虎林","fullname":"虎林市","level":"district","filename":"230000/230300/230381"},{"code":230382,"name":"密山","fullname":"密山市","level":"district","filename":"230000/230300/230382"}]},{"code":230400,"name":"鹤岗","fullname":"鹤岗市","level":"city","filename":"230000/230400","children":[{"code":230402,"name":"向阳","fullname":"向阳区","level":"district","filename":"230000/230400/230402"},{"code":230403,"name":"工农","fullname":"工农区","level":"district","filename":"230000/230400/230403"},{"code":230404,"name":"南山","fullname":"南山区","level":"district","filename":"230000/230400/230404"},{"code":230405,"name":"兴安","fullname":"兴安区","level":"district","filename":"230000/230400/230405"},{"code":230406,"name":"东山","fullname":"东山区","level":"district","filename":"230000/230400/230406"},{"code":230407,"name":"兴山","fullname":"兴山区","level":"district","filename":"230000/230400/230407"},{"code":230421,"name":"萝北","fullname":"萝北县","level":"district","filename":"230000/230400/230421"},{"code":230422,"name":"绥滨","fullname":"绥滨县","level":"district","filename":"230000/230400/230422"}]},{"code":230500,"name":"双鸭山","fullname":"双鸭山市","level":"city","filename":"230000/230500","children":[{"code":230502,"name":"尖山","fullname":"尖山区","level":"district","filename":"230000/230500/230502"},{"code":230503,"name":"岭东","fullname":"岭东区","level":"district","filename":"230000/230500/230503"},{"code":230505,"name":"四方台","fullname":"四方台区","level":"district","filename":"230000/230500/230505"},{"code":230506,"name":"宝山","fullname":"宝山区","level":"district","filename":"230000/230500/230506"},{"code":230521,"name":"集贤","fullname":"集贤县","level":"district","filename":"230000/230500/230521"},{"code":230522,"name":"友谊","fullname":"友谊县","level":"district","filename":"230000/230500/230522"},{"code":230523,"name":"宝清","fullname":"宝清县","level":"district","filename":"230000/230500/230523"},{"code":230524,"name":"饶河","fullname":"饶河县","level":"district","filename":"230000/230500/230524"}]},{"code":230600,"name":"大庆","fullname":"大庆市","level":"city","filename":"230000/230600","children":[{"code":230602,"name":"萨尔图","fullname":"萨尔图区","level":"district","filename":"230000/230600/230602"},{"code":230603,"name":"龙凤","fullname":"龙凤区","level":"district","filename":"230000/230600/230603"},{"code":230604,"name":"让胡路","fullname":"让胡路区","level":"district","filename":"230000/230600/230604"},{"code":230605,"name":"红岗","fullname":"红岗区","level":"district","filename":"230000/230600/230605"},{"code":230606,"name":"大同","fullname":"大同区","level":"district","filename":"230000/230600/230606"},{"code":230621,"name":"肇州","fullname":"肇州县","level":"district","filename":"230000/230600/230621"},{"code":230622,"name":"肇源","fullname":"肇源县","level":"district","filename":"230000/230600/230622"},{"code":230623,"name":"林甸","fullname":"林甸县","level":"district","filename":"230000/230600/230623"},{"code":230624,"name":"杜尔伯特","fullname":"杜尔伯特蒙古族自治县","level":"district","filename":"230000/230600/230624"}]},{"code":230700,"name":"伊春","fullname":"伊春市","level":"city","filename":"230000/230700","children":[{"code":230717,"name":"伊美","fullname":"伊美区","level":"district","filename":"230000/230700/230717"},{"code":230718,"name":"乌翠","fullname":"乌翠区","level":"district","filename":"230000/230700/230718"},{"code":230719,"name":"友好","fullname":"友好区","level":"district","filename":"230000/230700/230719"},{"code":230722,"name":"嘉荫","fullname":"嘉荫县","level":"district","filename":"230000/230700/230722"},{"code":230723,"name":"汤旺","fullname":"汤旺县","level":"district","filename":"230000/230700/230723"},{"code":230724,"name":"丰林","fullname":"丰林县","level":"district","filename":"230000/230700/230724"},{"code":230725,"name":"大箐山","fullname":"大箐山县","level":"district","filename":"230000/230700/230725"},{"code":230726,"name":"南岔","fullname":"南岔县","level":"district","filename":"230000/230700/230726"},{"code":230751,"name":"金林","fullname":"金林区","level":"district","filename":"230000/230700/230751"},{"code":230781,"name":"铁力","fullname":"铁力市","level":"district","filename":"230000/230700/230781"}]},{"code":230800,"name":"佳木斯","fullname":"佳木斯市","level":"city","filename":"230000/230800","children":[{"code":230803,"name":"向阳","fullname":"向阳区","level":"district","filename":"230000/230800/230803"},{"code":230804,"name":"前进","fullname":"前进区","level":"district","filename":"230000/230800/230804"},{"code":230805,"name":"东风","fullname":"东风区","level":"district","filename":"230000/230800/230805"},{"code":230811,"name":"郊区","fullname":"郊区","level":"district","filename":"230000/230800/230811"},{"code":230822,"name":"桦南","fullname":"桦南县","level":"district","filename":"230000/230800/230822"},{"code":230826,"name":"桦川","fullname":"桦川县","level":"district","filename":"230000/230800/230826"},{"code":230828,"name":"汤原","fullname":"汤原县","level":"district","filename":"230000/230800/230828"},{"code":230881,"name":"同江","fullname":"同江市","level":"district","filename":"230000/230800/230881"},{"code":230882,"name":"富锦","fullname":"富锦市","level":"district","filename":"230000/230800/230882"},{"code":230883,"name":"抚远","fullname":"抚远市","level":"district","filename":"230000/230800/230883"}]},{"code":230900,"name":"七台河","fullname":"七台河市","level":"city","filename":"230000/230900","children":[{"code":230902,"name":"新兴","fullname":"新兴区","level":"district","filename":"230000/230900/230902"},{"code":230903,"name":"桃山","fullname":"桃山区","level":"district","filename":"230000/230900/230903"},{"code":230904,"name":"茄子河","fullname":"茄子河区","level":"district","filename":"230000/230900/230904"},{"code":230921,"name":"勃利","fullname":"勃利县","level":"district","filename":"230000/230900/230921"}]},{"code":231000,"name":"牡丹江","fullname":"牡丹江市","level":"city","filename":"230000/231000","children":[{"code":231002,"name":"东安","fullname":"东安区","level":"district","filename":"230000/231000/231002"},{"code":231003,"name":"阳明","fullname":"阳明区","level":"district","filename":"230000/231000/231003"},{"code":231004,"name":"爱民","fullname":"爱民区","level":"district","filename":"230000/231000/231004"},{"code":231005,"name":"西安","fullname":"西安区","level":"district","filename":"230000/231000/231005"},{"code":231025,"name":"林口","fullname":"林口县","level":"district","filename":"230000/231000/231025"},{"code":231081,"name":"绥芬河","fullname":"绥芬河市","level":"district","filename":"230000/231000/231081"},{"code":231083,"name":"海林","fullname":"海林市","level":"district","filename":"230000/231000/231083"},{"code":231084,"name":"宁安","fullname":"宁安市","level":"district","filename":"230000/231000/231084"},{"code":231085,"name":"穆棱","fullname":"穆棱市","level":"district","filename":"230000/231000/231085"},{"code":231086,"name":"东宁","fullname":"东宁市","level":"district","filename":"230000/231000/231086"}]},{"code":231100,"name":"黑河","fullname":"黑河市","level":"city","filename":"230000/231100","children":[{"code":231102,"name":"爱辉","fullname":"爱辉区","level":"district","filename":"230000/231100/231102"},{"code":231123,"name":"逊克","fullname":"逊克县","level":"district","filename":"230000/231100/231123"},{"code":231124,"name":"孙吴","fullname":"孙吴县","level":"district","filename":"230000/231100/231124"},{"code":231181,"name":"北安","fullname":"北安市","level":"district","filename":"230000/231100/231181"},{"code":231182,"name":"五大连池","fullname":"五大连池市","level":"district","filename":"230000/231100/231182"},{"code":231183,"name":"嫩江","fullname":"嫩江市","level":"district","filename":"230000/231100/231183"}]},{"code":231200,"name":"绥化","fullname":"绥化市","level":"city","filename":"230000/231200","children":[{"code":231202,"name":"北林","fullname":"北林区","level":"district","filename":"230000/231200/231202"},{"code":231221,"name":"望奎","fullname":"望奎县","level":"district","filename":"230000/231200/231221"},{"code":231222,"name":"兰西","fullname":"兰西县","level":"district","filename":"230000/231200/231222"},{"code":231223,"name":"青冈","fullname":"青冈县","level":"district","filename":"230000/231200/231223"},{"code":231224,"name":"庆安","fullname":"庆安县","level":"district","filename":"230000/231200/231224"},{"code":231225,"name":"明水","fullname":"明水县","level":"district","filename":"230000/231200/231225"},{"code":231226,"name":"绥棱","fullname":"绥棱县","level":"district","filename":"230000/231200/231226"},{"code":231281,"name":"安达","fullname":"安达市","level":"district","filename":"230000/231200/231281"},{"code":231282,"name":"肇东","fullname":"肇东市","level":"district","filename":"230000/231200/231282"},{"code":231283,"name":"海伦","fullname":"海伦市","level":"district","filename":"230000/231200/231283"}]},{"code":232700,"name":"大兴安岭","fullname":"大兴安岭地区","level":"city","filename":"230000/232700","children":[{"code":232701,"name":"漠河","fullname":"漠河市","level":"district","filename":"230000/232700/232701"},{"code":232718,"name":"加格达奇","fullname":"加格达奇区","level":"district","filename":"230000/232700/232718"},{"code":232721,"name":"呼玛","fullname":"呼玛县","level":"district","filename":"230000/232700/232721"},{"code":232722,"name":"塔河","fullname":"塔河县","level":"district","filename":"230000/232700/232722"}]}]},{"code":310000,"name":"上海","fullname":"上海市","level":"province","filename":"310000","children":[{"code":310101,"name":"黄浦","fullname":"黄浦区","level":"district","filename":"310000/310101"},{"code":310104,"name":"徐汇","fullname":"徐汇区","level":"district","filename":"310000/310104"},{"code":310105,"name":"长宁","fullname":"长宁区","level":"district","filename":"310000/310105"},{"code":310106,"name":"静安","fullname":"静安区","level":"district","filename":"310000/310106"},{"code":310107,"name":"普陀","fullname":"普陀区","level":"district","filename":"310000/310107"},{"code":310109,"name":"虹口","fullname":"虹口区","level":"district","filename":"310000/310109"},{"code":310110,"name":"杨浦","fullname":"杨浦区","level":"district","filename":"310000/310110"},{"code":310112,"name":"闵行","fullname":"闵行区","level":"district","filename":"310000/310112"},{"code":310113,"name":"宝山","fullname":"宝山区","level":"district","filename":"310000/310113"},{"code":310114,"name":"嘉定","fullname":"嘉定区","level":"district","filename":"310000/310114"},{"code":310115,"name":"浦东","fullname":"浦东新区","level":"district","filename":"310000/310115"},{"code":310116,"name":"金山","fullname":"金山区","level":"district","filename":"310000/310116"},{"code":310117,"name":"松江","fullname":"松江区","level":"district","filename":"310000/310117"},{"code":310118,"name":"青浦","fullname":"青浦区","level":"district","filename":"310000/310118"},{"code":310120,"name":"奉贤","fullname":"奉贤区","level":"district","filename":"310000/310120"},{"code":310151,"name":"崇明","fullname":"崇明区","level":"district","filename":"310000/310151"}]},{"code":320000,"name":"江苏","fullname":"江苏省","level":"province","filename":"320000","children":[{"code":320100,"name":"南京","fullname":"南京市","level":"city","filename":"320000/320100","children":[{"code":320102,"name":"玄武","fullname":"玄武区","level":"district","filename":"320000/320100/320102"},{"code":320104,"name":"秦淮","fullname":"秦淮区","level":"district","filename":"320000/320100/320104"},{"code":320105,"name":"建邺","fullname":"建邺区","level":"district","filename":"320000/320100/320105"},{"code":320106,"name":"鼓楼","fullname":"鼓楼区","level":"district","filename":"320000/320100/320106"},{"code":320111,"name":"浦口","fullname":"浦口区","level":"district","filename":"320000/320100/320111"},{"code":320113,"name":"栖霞","fullname":"栖霞区","level":"district","filename":"320000/320100/320113"},{"code":320114,"name":"雨花台","fullname":"雨花台区","level":"district","filename":"320000/320100/320114"},{"code":320115,"name":"江宁","fullname":"江宁区","level":"district","filename":"320000/320100/320115"},{"code":320116,"name":"六合","fullname":"六合区","level":"district","filename":"320000/320100/320116"},{"code":320117,"name":"溧水","fullname":"溧水区","level":"district","filename":"320000/320100/320117"},{"code":320118,"name":"高淳","fullname":"高淳区","level":"district","filename":"320000/320100/320118"}]},{"code":320200,"name":"无锡","fullname":"无锡市","level":"city","filename":"320000/320200","children":[{"code":320205,"name":"锡山","fullname":"锡山区","level":"district","filename":"320000/320200/320205"},{"code":320206,"name":"惠山","fullname":"惠山区","level":"district","filename":"320000/320200/320206"},{"code":320211,"name":"滨湖","fullname":"滨湖区","level":"district","filename":"320000/320200/320211"},{"code":320213,"name":"梁溪","fullname":"梁溪区","level":"district","filename":"320000/320200/320213"},{"code":320214,"name":"新吴","fullname":"新吴区","level":"district","filename":"320000/320200/320214"},{"code":320281,"name":"江阴","fullname":"江阴市","level":"district","filename":"320000/320200/320281"},{"code":320282,"name":"宜兴","fullname":"宜兴市","level":"district","filename":"320000/320200/320282"}]},{"code":320300,"name":"徐州","fullname":"徐州市","level":"city","filename":"320000/320300","children":[{"code":320302,"name":"鼓楼","fullname":"鼓楼区","level":"district","filename":"320000/320300/320302"},{"code":320303,"name":"云龙","fullname":"云龙区","level":"district","filename":"320000/320300/320303"},{"code":320305,"name":"贾汪","fullname":"贾汪区","level":"district","filename":"320000/320300/320305"},{"code":320311,"name":"泉山","fullname":"泉山区","level":"district","filename":"320000/320300/320311"},{"code":320312,"name":"铜山","fullname":"铜山区","level":"district","filename":"320000/320300/320312"},{"code":320321,"name":"丰县","fullname":"丰县","level":"district","filename":"320000/320300/320321"},{"code":320322,"name":"沛县","fullname":"沛县","level":"district","filename":"320000/320300/320322"},{"code":320324,"name":"睢宁","fullname":"睢宁县","level":"district","filename":"320000/320300/320324"},{"code":320381,"name":"新沂","fullname":"新沂市","level":"district","filename":"320000/320300/320381"},{"code":320382,"name":"邳州","fullname":"邳州市","level":"district","filename":"320000/320300/320382"}]},{"code":320400,"name":"常州","fullname":"常州市","level":"city","filename":"320000/320400","children":[{"code":320402,"name":"天宁","fullname":"天宁区","level":"district","filename":"320000/320400/320402"},{"code":320404,"name":"钟楼","fullname":"钟楼区","level":"district","filename":"320000/320400/320404"},{"code":320411,"name":"新北","fullname":"新北区","level":"district","filename":"320000/320400/320411"},{"code":320412,"name":"武进","fullname":"武进区","level":"district","filename":"320000/320400/320412"},{"code":320413,"name":"金坛","fullname":"金坛区","level":"district","filename":"320000/320400/320413"},{"code":320481,"name":"溧阳","fullname":"溧阳市","level":"district","filename":"320000/320400/320481"}]},{"code":320500,"name":"苏州","fullname":"苏州市","level":"city","filename":"320000/320500","children":[{"code":320505,"name":"虎丘","fullname":"虎丘区","level":"district","filename":"320000/320500/320505"},{"code":320506,"name":"吴中","fullname":"吴中区","level":"district","filename":"320000/320500/320506"},{"code":320507,"name":"相城","fullname":"相城区","level":"district","filename":"320000/320500/320507"},{"code":320508,"name":"姑苏","fullname":"姑苏区","level":"district","filename":"320000/320500/320508"},{"code":320509,"name":"吴江","fullname":"吴江区","level":"district","filename":"320000/320500/320509"},{"code":320581,"name":"常熟","fullname":"常熟市","level":"district","filename":"320000/320500/320581"},{"code":320582,"name":"张家港","fullname":"张家港市","level":"district","filename":"320000/320500/320582"},{"code":320583,"name":"昆山","fullname":"昆山市","level":"district","filename":"320000/320500/320583"},{"code":320585,"name":"太仓","fullname":"太仓市","level":"district","filename":"320000/320500/320585"}]},{"code":320600,"name":"南通","fullname":"南通市","level":"city","filename":"320000/320600","children":[{"code":320602,"name":"崇川","fullname":"崇川区","level":"district","filename":"320000/320600/320602"},{"code":320612,"name":"通州","fullname":"通州区","level":"district","filename":"320000/320600/320612"},{"code":320623,"name":"如东","fullname":"如东县","level":"district","filename":"320000/320600/320623"},{"code":320681,"name":"启东","fullname":"启东市","level":"district","filename":"320000/320600/320681"},{"code":320682,"name":"如皋","fullname":"如皋市","level":"district","filename":"320000/320600/320682"},{"code":320684,"name":"海门","fullname":"海门区","level":"district","filename":"320000/320600/320684"},{"code":320685,"name":"海安","fullname":"海安市","level":"district","filename":"320000/320600/320685"}]},{"code":320700,"name":"连云港","fullname":"连云港市","level":"city","filename":"320000/320700","children":[{"code":320703,"name":"连云","fullname":"连云区","level":"district","filename":"320000/320700/320703"},{"code":320706,"name":"海州","fullname":"海州区","level":"district","filename":"320000/320700/320706"},{"code":320707,"name":"赣榆","fullname":"赣榆区","level":"district","filename":"320000/320700/320707"},{"code":320722,"name":"东海","fullname":"东海县","level":"district","filename":"320000/320700/320722"},{"code":320723,"name":"灌云","fullname":"灌云县","level":"district","filename":"320000/320700/320723"},{"code":320724,"name":"灌南","fullname":"灌南县","level":"district","filename":"320000/320700/320724"}]},{"code":320800,"name":"淮安","fullname":"淮安市","level":"city","filename":"320000/320800","children":[{"code":320803,"name":"淮安","fullname":"淮安区","level":"district","filename":"320000/320800/320803"},{"code":320804,"name":"淮阴","fullname":"淮阴区","level":"district","filename":"320000/320800/320804"},{"code":320812,"name":"清江浦","fullname":"清江浦区","level":"district","filename":"320000/320800/320812"},{"code":320813,"name":"洪泽","fullname":"洪泽区","level":"district","filename":"320000/320800/320813"},{"code":320826,"name":"涟水","fullname":"涟水县","level":"district","filename":"320000/320800/320826"},{"code":320830,"name":"盱眙","fullname":"盱眙县","level":"district","filename":"320000/320800/320830"},{"code":320831,"name":"金湖","fullname":"金湖县","level":"district","filename":"320000/320800/320831"}]},{"code":320900,"name":"盐城","fullname":"盐城市","level":"city","filename":"320000/320900","children":[{"code":320902,"name":"亭湖","fullname":"亭湖区","level":"district","filename":"320000/320900/320902"},{"code":320903,"name":"盐都","fullname":"盐都区","level":"district","filename":"320000/320900/320903"},{"code":320904,"name":"大丰","fullname":"大丰区","level":"district","filename":"320000/320900/320904"},{"code":320921,"name":"响水","fullname":"响水县","level":"district","filename":"320000/320900/320921"},{"code":320922,"name":"滨海","fullname":"滨海县","level":"district","filename":"320000/320900/320922"},{"code":320923,"name":"阜宁","fullname":"阜宁县","level":"district","filename":"320000/320900/320923"},{"code":320924,"name":"射阳","fullname":"射阳县","level":"district","filename":"320000/320900/320924"},{"code":320925,"name":"建湖","fullname":"建湖县","level":"district","filename":"320000/320900/320925"},{"code":320981,"name":"东台","fullname":"东台市","level":"district","filename":"320000/320900/320981"}]},{"code":321000,"name":"扬州","fullname":"扬州市","level":"city","filename":"320000/321000","children":[{"code":321002,"name":"广陵","fullname":"广陵区","level":"district","filename":"320000/321000/321002"},{"code":321003,"name":"邗江","fullname":"邗江区","level":"district","filename":"320000/321000/321003"},{"code":321012,"name":"江都","fullname":"江都区","level":"district","filename":"320000/321000/321012"},{"code":321023,"name":"宝应","fullname":"宝应县","level":"district","filename":"320000/321000/321023"},{"code":321081,"name":"仪征","fullname":"仪征市","level":"district","filename":"320000/321000/321081"},{"code":321084,"name":"高邮","fullname":"高邮市","level":"district","filename":"320000/321000/321084"}]},{"code":321100,"name":"镇江","fullname":"镇江市","level":"city","filename":"320000/321100","children":[{"code":321102,"name":"京口","fullname":"京口区","level":"district","filename":"320000/321100/321102"},{"code":321111,"name":"润州","fullname":"润州区","level":"district","filename":"320000/321100/321111"},{"code":321112,"name":"丹徒","fullname":"丹徒区","level":"district","filename":"320000/321100/321112"},{"code":321181,"name":"丹阳","fullname":"丹阳市","level":"district","filename":"320000/321100/321181"},{"code":321182,"name":"扬中","fullname":"扬中市","level":"district","filename":"320000/321100/321182"},{"code":321183,"name":"句容","fullname":"句容市","level":"district","filename":"320000/321100/321183"}]},{"code":321200,"name":"泰州","fullname":"泰州市","level":"city","filename":"320000/321200","children":[{"code":321202,"name":"海陵","fullname":"海陵区","level":"district","filename":"320000/321200/321202"},{"code":321203,"name":"高港","fullname":"高港区","level":"district","filename":"320000/321200/321203"},{"code":321204,"name":"姜堰","fullname":"姜堰区","level":"district","filename":"320000/321200/321204"},{"code":321281,"name":"兴化","fullname":"兴化市","level":"district","filename":"320000/321200/321281"},{"code":321282,"name":"靖江","fullname":"靖江市","level":"district","filename":"320000/321200/321282"},{"code":321283,"name":"泰兴","fullname":"泰兴市","level":"district","filename":"320000/321200/321283"}]},{"code":321300,"name":"宿迁","fullname":"宿迁市","level":"city","filename":"320000/321300","children":[{"code":321302,"name":"宿城","fullname":"宿城区","level":"district","filename":"320000/321300/321302"},{"code":321311,"name":"宿豫","fullname":"宿豫区","level":"district","filename":"320000/321300/321311"},{"code":321322,"name":"沭阳","fullname":"沭阳县","level":"district","filename":"320000/321300/321322"},{"code":321323,"name":"泗阳","fullname":"泗阳县","level":"district","filename":"320000/321300/321323"},{"code":321324,"name":"泗洪","fullname":"泗洪县","level":"district","filename":"320000/321300/321324"}]}]},{"code":330000,"name":"浙江","fullname":"浙江省","level":"province","filename":"330000","children":[{"code":330100,"name":"杭州","fullname":"杭州市","level":"city","filename":"330000/330100","children":[{"code":330102,"name":"上城","fullname":"上城区","level":"district","filename":"330000/330100/330102"},{"code":330105,"name":"拱墅","fullname":"拱墅区","level":"district","filename":"330000/330100/330105"},{"code":330106,"name":"西湖","fullname":"西湖区","level":"district","filename":"330000/330100/330106"},{"code":330108,"name":"滨江","fullname":"滨江区","level":"district","filename":"330000/330100/330108"},{"code":330109,"name":"萧山","fullname":"萧山区","level":"district","filename":"330000/330100/330109"},{"code":330110,"name":"余杭","fullname":"余杭区","level":"district","filename":"330000/330100/330110"},{"code":330111,"name":"富阳","fullname":"富阳区","level":"district","filename":"330000/330100/330111"},{"code":330112,"name":"临安","fullname":"临安区","level":"district","filename":"330000/330100/330112"},{"code":330114,"name":"钱塘","fullname":"钱塘区","level":"district","filename":"330000/330100/330114"},{"code":330113,"name":"临平","fullname":"临平区","level":"district","filename":"330000/330100/330113"},{"code":330122,"name":"桐庐","fullname":"桐庐县","level":"district","filename":"330000/330100/330122"},{"code":330127,"name":"淳安","fullname":"淳安县","level":"district","filename":"330000/330100/330127"},{"code":330182,"name":"建德","fullname":"建德市","level":"district","filename":"330000/330100/330182"}]},{"code":330200,"name":"宁波","fullname":"宁波市","level":"city","filename":"330000/330200","children":[{"code":330203,"name":"海曙","fullname":"海曙区","level":"district","filename":"330000/330200/330203"},{"code":330205,"name":"江北","fullname":"江北区","level":"district","filename":"330000/330200/330205"},{"code":330206,"name":"北仑","fullname":"北仑区","level":"district","filename":"330000/330200/330206"},{"code":330211,"name":"镇海","fullname":"镇海区","level":"district","filename":"330000/330200/330211"},{"code":330212,"name":"鄞州","fullname":"鄞州区","level":"district","filename":"330000/330200/330212"},{"code":330213,"name":"奉化","fullname":"奉化区","level":"district","filename":"330000/330200/330213"},{"code":330225,"name":"象山","fullname":"象山县","level":"district","filename":"330000/330200/330225"},{"code":330226,"name":"宁海","fullname":"宁海县","level":"district","filename":"330000/330200/330226"},{"code":330281,"name":"余姚","fullname":"余姚市","level":"district","filename":"330000/330200/330281"},{"code":330282,"name":"慈溪","fullname":"慈溪市","level":"district","filename":"330000/330200/330282"}]},{"code":330300,"name":"温州","fullname":"温州市","level":"city","filename":"330000/330300","children":[{"code":330302,"name":"鹿城","fullname":"鹿城区","level":"district","filename":"330000/330300/330302"},{"code":330303,"name":"龙湾","fullname":"龙湾区","level":"district","filename":"330000/330300/330303"},{"code":330304,"name":"瓯海","fullname":"瓯海区","level":"district","filename":"330000/330300/330304"},{"code":330305,"name":"洞头","fullname":"洞头区","level":"district","filename":"330000/330300/330305"},{"code":330324,"name":"永嘉","fullname":"永嘉县","level":"district","filename":"330000/330300/330324"},{"code":330326,"name":"平阳","fullname":"平阳县","level":"district","filename":"330000/330300/330326"},{"code":330327,"name":"苍南","fullname":"苍南县","level":"district","filename":"330000/330300/330327"},{"code":330328,"name":"文成","fullname":"文成县","level":"district","filename":"330000/330300/330328"},{"code":330329,"name":"泰顺","fullname":"泰顺县","level":"district","filename":"330000/330300/330329"},{"code":330381,"name":"瑞安","fullname":"瑞安市","level":"district","filename":"330000/330300/330381"},{"code":330382,"name":"乐清","fullname":"乐清市","level":"district","filename":"330000/330300/330382"},{"code":330383,"name":"龙港","fullname":"龙港市","level":"district","filename":"330000/330300/330383"}]},{"code":330400,"name":"嘉兴","fullname":"嘉兴市","level":"city","filename":"330000/330400","children":[{"code":330402,"name":"南湖","fullname":"南湖区","level":"district","filename":"330000/330400/330402"},{"code":330411,"name":"秀洲","fullname":"秀洲区","level":"district","filename":"330000/330400/330411"},{"code":330421,"name":"嘉善","fullname":"嘉善县","level":"district","filename":"330000/330400/330421"},{"code":330424,"name":"海盐","fullname":"海盐县","level":"district","filename":"330000/330400/330424"},{"code":330481,"name":"海宁","fullname":"海宁市","level":"district","filename":"330000/330400/330481"},{"code":330482,"name":"平湖","fullname":"平湖市","level":"district","filename":"330000/330400/330482"},{"code":330483,"name":"桐乡","fullname":"桐乡市","level":"district","filename":"330000/330400/330483"}]},{"code":330500,"name":"湖州","fullname":"湖州市","level":"city","filename":"330000/330500","children":[{"code":330502,"name":"吴兴","fullname":"吴兴区","level":"district","filename":"330000/330500/330502"},{"code":330503,"name":"南浔","fullname":"南浔区","level":"district","filename":"330000/330500/330503"},{"code":330521,"name":"德清","fullname":"德清县","level":"district","filename":"330000/330500/330521"},{"code":330522,"name":"长兴","fullname":"长兴县","level":"district","filename":"330000/330500/330522"},{"code":330523,"name":"安吉","fullname":"安吉县","level":"district","filename":"330000/330500/330523"}]},{"code":330600,"name":"绍兴","fullname":"绍兴市","level":"city","filename":"330000/330600","children":[{"code":330602,"name":"越城","fullname":"越城区","level":"district","filename":"330000/330600/330602"},{"code":330603,"name":"柯桥","fullname":"柯桥区","level":"district","filename":"330000/330600/330603"},{"code":330604,"name":"上虞","fullname":"上虞区","level":"district","filename":"330000/330600/330604"},{"code":330624,"name":"新昌","fullname":"新昌县","level":"district","filename":"330000/330600/330624"},{"code":330681,"name":"诸暨","fullname":"诸暨市","level":"district","filename":"330000/330600/330681"},{"code":330683,"name":"嵊州","fullname":"嵊州市","level":"district","filename":"330000/330600/330683"}]},{"code":330700,"name":"金华","fullname":"金华市","level":"city","filename":"330000/330700","children":[{"code":330702,"name":"婺城","fullname":"婺城区","level":"district","filename":"330000/330700/330702"},{"code":330703,"name":"金东","fullname":"金东区","level":"district","filename":"330000/330700/330703"},{"code":330723,"name":"武义","fullname":"武义县","level":"district","filename":"330000/330700/330723"},{"code":330726,"name":"浦江","fullname":"浦江县","level":"district","filename":"330000/330700/330726"},{"code":330727,"name":"磐安","fullname":"磐安县","level":"district","filename":"330000/330700/330727"},{"code":330781,"name":"兰溪","fullname":"兰溪市","level":"district","filename":"330000/330700/330781"},{"code":330782,"name":"义乌","fullname":"义乌市","level":"district","filename":"330000/330700/330782"},{"code":330783,"name":"东阳","fullname":"东阳市","level":"district","filename":"330000/330700/330783"},{"code":330784,"name":"永康","fullname":"永康市","level":"district","filename":"330000/330700/330784"}]},{"code":330800,"name":"衢州","fullname":"衢州市","level":"city","filename":"330000/330800","children":[{"code":330802,"name":"柯城","fullname":"柯城区","level":"district","filename":"330000/330800/330802"},{"code":330803,"name":"衢江","fullname":"衢江区","level":"district","filename":"330000/330800/330803"},{"code":330822,"name":"常山","fullname":"常山县","level":"district","filename":"330000/330800/330822"},{"code":330824,"name":"开化","fullname":"开化县","level":"district","filename":"330000/330800/330824"},{"code":330825,"name":"龙游","fullname":"龙游县","level":"district","filename":"330000/330800/330825"},{"code":330881,"name":"江山","fullname":"江山市","level":"district","filename":"330000/330800/330881"}]},{"code":330900,"name":"舟山","fullname":"舟山市","level":"city","filename":"330000/330900","children":[{"code":330902,"name":"定海","fullname":"定海区","level":"district","filename":"330000/330900/330902"},{"code":330903,"name":"普陀","fullname":"普陀区","level":"district","filename":"330000/330900/330903"},{"code":330921,"name":"岱山","fullname":"岱山县","level":"district","filename":"330000/330900/330921"},{"code":330922,"name":"嵊泗","fullname":"嵊泗县","level":"district","filename":"330000/330900/330922"}]},{"code":331000,"name":"台州","fullname":"台州市","level":"city","filename":"330000/331000","children":[{"code":331002,"name":"椒江","fullname":"椒江区","level":"district","filename":"330000/331000/331002"},{"code":331003,"name":"黄岩","fullname":"黄岩区","level":"district","filename":"330000/331000/331003"},{"code":331004,"name":"路桥","fullname":"路桥区","level":"district","filename":"330000/331000/331004"},{"code":331022,"name":"三门","fullname":"三门县","level":"district","filename":"330000/331000/331022"},{"code":331023,"name":"天台","fullname":"天台县","level":"district","filename":"330000/331000/331023"},{"code":331024,"name":"仙居","fullname":"仙居县","level":"district","filename":"330000/331000/331024"},{"code":331081,"name":"温岭","fullname":"温岭市","level":"district","filename":"330000/331000/331081"},{"code":331082,"name":"临海","fullname":"临海市","level":"district","filename":"330000/331000/331082"},{"code":331083,"name":"玉环","fullname":"玉环市","level":"district","filename":"330000/331000/331083"}]},{"code":331100,"name":"丽水","fullname":"丽水市","level":"city","filename":"330000/331100","children":[{"code":331102,"name":"莲都","fullname":"莲都区","level":"district","filename":"330000/331100/331102"},{"code":331121,"name":"青田","fullname":"青田县","level":"district","filename":"330000/331100/331121"},{"code":331122,"name":"缙云","fullname":"缙云县","level":"district","filename":"330000/331100/331122"},{"code":331123,"name":"遂昌","fullname":"遂昌县","level":"district","filename":"330000/331100/331123"},{"code":331124,"name":"松阳","fullname":"松阳县","level":"district","filename":"330000/331100/331124"},{"code":331125,"name":"云和","fullname":"云和县","level":"district","filename":"330000/331100/331125"},{"code":331126,"name":"庆元","fullname":"庆元县","level":"district","filename":"330000/331100/331126"},{"code":331127,"name":"景宁","fullname":"景宁畲族自治县","level":"district","filename":"330000/331100/331127"},{"code":331181,"name":"龙泉","fullname":"龙泉市","level":"district","filename":"330000/331100/331181"}]}]},{"code":340000,"name":"安徽","fullname":"安徽省","level":"province","filename":"340000","children":[{"code":340100,"name":"合肥","fullname":"合肥市","level":"city","filename":"340000/340100","children":[{"code":340102,"name":"瑶海","fullname":"瑶海区","level":"district","filename":"340000/340100/340102"},{"code":340103,"name":"庐阳","fullname":"庐阳区","level":"district","filename":"340000/340100/340103"},{"code":340104,"name":"蜀山","fullname":"蜀山区","level":"district","filename":"340000/340100/340104"},{"code":340111,"name":"包河","fullname":"包河区","level":"district","filename":"340000/340100/340111"},{"code":340121,"name":"长丰","fullname":"长丰县","level":"district","filename":"340000/340100/340121"},{"code":340122,"name":"肥东","fullname":"肥东县","level":"district","filename":"340000/340100/340122"},{"code":340123,"name":"肥西","fullname":"肥西县","level":"district","filename":"340000/340100/340123"},{"code":340124,"name":"庐江","fullname":"庐江县","level":"district","filename":"340000/340100/340124"},{"code":340181,"name":"巢湖","fullname":"巢湖市","level":"district","filename":"340000/340100/340181"}]},{"code":340200,"name":"芜湖","fullname":"芜湖市","level":"city","filename":"340000/340200","children":[{"code":340202,"name":"镜湖","fullname":"镜湖区","level":"district","filename":"340000/340200/340202"},{"code":340207,"name":"鸠江","fullname":"鸠江区","level":"district","filename":"340000/340200/340207"},{"code":340209,"name":"弋江","fullname":"弋江区","level":"district","filename":"340000/340200/340209"},{"code":340210,"name":"湾沚","fullname":"湾沚区","level":"district","filename":"340000/340200/340210"},{"code":340211,"name":"繁昌","fullname":"繁昌区","level":"district","filename":"340000/340200/340211"},{"code":340223,"name":"南陵","fullname":"南陵县","level":"district","filename":"340000/340200/340223"},{"code":340281,"name":"无为","fullname":"无为市","level":"district","filename":"340000/340200/340281"}]},{"code":340300,"name":"蚌埠","fullname":"蚌埠市","level":"city","filename":"340000/340300","children":[{"code":340302,"name":"龙子湖","fullname":"龙子湖区","level":"district","filename":"340000/340300/340302"},{"code":340303,"name":"蚌山","fullname":"蚌山区","level":"district","filename":"340000/340300/340303"},{"code":340304,"name":"禹会","fullname":"禹会区","level":"district","filename":"340000/340300/340304"},{"code":340311,"name":"淮上","fullname":"淮上区","level":"district","filename":"340000/340300/340311"},{"code":340321,"name":"怀远","fullname":"怀远县","level":"district","filename":"340000/340300/340321"},{"code":340322,"name":"五河","fullname":"五河县","level":"district","filename":"340000/340300/340322"},{"code":340323,"name":"固镇","fullname":"固镇县","level":"district","filename":"340000/340300/340323"}]},{"code":340400,"name":"淮南","fullname":"淮南市","level":"city","filename":"340000/340400","children":[{"code":340402,"name":"大通","fullname":"大通区","level":"district","filename":"340000/340400/340402"},{"code":340403,"name":"田家庵","fullname":"田家庵区","level":"district","filename":"340000/340400/340403"},{"code":340404,"name":"谢家集","fullname":"谢家集区","level":"district","filename":"340000/340400/340404"},{"code":340405,"name":"八公山","fullname":"八公山区","level":"district","filename":"340000/340400/340405"},{"code":340406,"name":"潘集","fullname":"潘集区","level":"district","filename":"340000/340400/340406"},{"code":340421,"name":"凤台","fullname":"凤台县","level":"district","filename":"340000/340400/340421"},{"code":340422,"name":"寿县","fullname":"寿县","level":"district","filename":"340000/340400/340422"}]},{"code":340500,"name":"马鞍山","fullname":"马鞍山市","level":"city","filename":"340000/340500","children":[{"code":340503,"name":"花山","fullname":"花山区","level":"district","filename":"340000/340500/340503"},{"code":340504,"name":"雨山","fullname":"雨山区","level":"district","filename":"340000/340500/340504"},{"code":340506,"name":"博望","fullname":"博望区","level":"district","filename":"340000/340500/340506"},{"code":340521,"name":"当涂","fullname":"当涂县","level":"district","filename":"340000/340500/340521"},{"code":340522,"name":"含山","fullname":"含山县","level":"district","filename":"340000/340500/340522"},{"code":340523,"name":"和县","fullname":"和县","level":"district","filename":"340000/340500/340523"}]},{"code":340600,"name":"淮北","fullname":"淮北市","level":"city","filename":"340000/340600","children":[{"code":340602,"name":"杜集","fullname":"杜集区","level":"district","filename":"340000/340600/340602"},{"code":340603,"name":"相山","fullname":"相山区","level":"district","filename":"340000/340600/340603"},{"code":340604,"name":"烈山","fullname":"烈山区","level":"district","filename":"340000/340600/340604"},{"code":340621,"name":"濉溪","fullname":"濉溪县","level":"district","filename":"340000/340600/340621"}]},{"code":340700,"name":"铜陵","fullname":"铜陵市","level":"city","filename":"340000/340700","children":[{"code":340705,"name":"铜官","fullname":"铜官区","level":"district","filename":"340000/340700/340705"},{"code":340706,"name":"义安","fullname":"义安区","level":"district","filename":"340000/340700/340706"},{"code":340711,"name":"郊区","fullname":"郊区","level":"district","filename":"340000/340700/340711"},{"code":340722,"name":"枞阳","fullname":"枞阳县","level":"district","filename":"340000/340700/340722"}]},{"code":340800,"name":"安庆","fullname":"安庆市","level":"city","filename":"340000/340800","children":[{"code":340802,"name":"迎江","fullname":"迎江区","level":"district","filename":"340000/340800/340802"},{"code":340803,"name":"大观","fullname":"大观区","level":"district","filename":"340000/340800/340803"},{"code":340811,"name":"宜秀","fullname":"宜秀区","level":"district","filename":"340000/340800/340811"},{"code":340822,"name":"怀宁","fullname":"怀宁县","level":"district","filename":"340000/340800/340822"},{"code":340825,"name":"太湖","fullname":"太湖县","level":"district","filename":"340000/340800/340825"},{"code":340826,"name":"宿松","fullname":"宿松县","level":"district","filename":"340000/340800/340826"},{"code":340827,"name":"望江","fullname":"望江县","level":"district","filename":"340000/340800/340827"},{"code":340828,"name":"岳西","fullname":"岳西县","level":"district","filename":"340000/340800/340828"},{"code":340881,"name":"桐城","fullname":"桐城市","level":"district","filename":"340000/340800/340881"},{"code":340882,"name":"潜山","fullname":"潜山市","level":"district","filename":"340000/340800/340882"}]},{"code":341000,"name":"黄山","fullname":"黄山市","level":"city","filename":"340000/341000","children":[{"code":341002,"name":"屯溪","fullname":"屯溪区","level":"district","filename":"340000/341000/341002"},{"code":341003,"name":"黄山","fullname":"黄山区","level":"district","filename":"340000/341000/341003"},{"code":341004,"name":"徽州","fullname":"徽州区","level":"district","filename":"340000/341000/341004"},{"code":341021,"name":"歙县","fullname":"歙县","level":"district","filename":"340000/341000/341021"},{"code":341022,"name":"休宁","fullname":"休宁县","level":"district","filename":"340000/341000/341022"},{"code":341023,"name":"黟县","fullname":"黟县","level":"district","filename":"340000/341000/341023"},{"code":341024,"name":"祁门","fullname":"祁门县","level":"district","filename":"340000/341000/341024"}]},{"code":341100,"name":"滁州","fullname":"滁州市","level":"city","filename":"340000/341100","children":[{"code":341102,"name":"琅琊","fullname":"琅琊区","level":"district","filename":"340000/341100/341102"},{"code":341103,"name":"南谯","fullname":"南谯区","level":"district","filename":"340000/341100/341103"},{"code":341122,"name":"来安","fullname":"来安县","level":"district","filename":"340000/341100/341122"},{"code":341124,"name":"全椒","fullname":"全椒县","level":"district","filename":"340000/341100/341124"},{"code":341125,"name":"定远","fullname":"定远县","level":"district","filename":"340000/341100/341125"},{"code":341126,"name":"凤阳","fullname":"凤阳县","level":"district","filename":"340000/341100/341126"},{"code":341181,"name":"天长","fullname":"天长市","level":"district","filename":"340000/341100/341181"},{"code":341182,"name":"明光","fullname":"明光市","level":"district","filename":"340000/341100/341182"}]},{"code":341200,"name":"阜阳","fullname":"阜阳市","level":"city","filename":"340000/341200","children":[{"code":341202,"name":"颍州","fullname":"颍州区","level":"district","filename":"340000/341200/341202"},{"code":341203,"name":"颍东","fullname":"颍东区","level":"district","filename":"340000/341200/341203"},{"code":341204,"name":"颍泉","fullname":"颍泉区","level":"district","filename":"340000/341200/341204"},{"code":341221,"name":"临泉","fullname":"临泉县","level":"district","filename":"340000/341200/341221"},{"code":341222,"name":"太和","fullname":"太和县","level":"district","filename":"340000/341200/341222"},{"code":341225,"name":"阜南","fullname":"阜南县","level":"district","filename":"340000/341200/341225"},{"code":341226,"name":"颍上","fullname":"颍上县","level":"district","filename":"340000/341200/341226"},{"code":341282,"name":"界首","fullname":"界首市","level":"district","filename":"340000/341200/341282"}]},{"code":341300,"name":"宿州","fullname":"宿州市","level":"city","filename":"340000/341300","children":[{"code":341302,"name":"埇桥","fullname":"埇桥区","level":"district","filename":"340000/341300/341302"},{"code":341321,"name":"砀山","fullname":"砀山县","level":"district","filename":"340000/341300/341321"},{"code":341322,"name":"萧县","fullname":"萧县","level":"district","filename":"340000/341300/341322"},{"code":341323,"name":"灵璧","fullname":"灵璧县","level":"district","filename":"340000/341300/341323"},{"code":341324,"name":"泗县","fullname":"泗县","level":"district","filename":"340000/341300/341324"}]},{"code":341500,"name":"六安","fullname":"六安市","level":"city","filename":"340000/341500","children":[{"code":341502,"name":"金安","fullname":"金安区","level":"district","filename":"340000/341500/341502"},{"code":341503,"name":"裕安","fullname":"裕安区","level":"district","filename":"340000/341500/341503"},{"code":341504,"name":"叶集","fullname":"叶集区","level":"district","filename":"340000/341500/341504"},{"code":341522,"name":"霍邱","fullname":"霍邱县","level":"district","filename":"340000/341500/341522"},{"code":341523,"name":"舒城","fullname":"舒城县","level":"district","filename":"340000/341500/341523"},{"code":341524,"name":"金寨","fullname":"金寨县","level":"district","filename":"340000/341500/341524"},{"code":341525,"name":"霍山","fullname":"霍山县","level":"district","filename":"340000/341500/341525"}]},{"code":341600,"name":"亳州","fullname":"亳州市","level":"city","filename":"340000/341600","children":[{"code":341602,"name":"谯城","fullname":"谯城区","level":"district","filename":"340000/341600/341602"},{"code":341621,"name":"涡阳","fullname":"涡阳县","level":"district","filename":"340000/341600/341621"},{"code":341622,"name":"蒙城","fullname":"蒙城县","level":"district","filename":"340000/341600/341622"},{"code":341623,"name":"利辛","fullname":"利辛县","level":"district","filename":"340000/341600/341623"}]},{"code":341700,"name":"池州","fullname":"池州市","level":"city","filename":"340000/341700","children":[{"code":341702,"name":"贵池","fullname":"贵池区","level":"district","filename":"340000/341700/341702"},{"code":341721,"name":"东至","fullname":"东至县","level":"district","filename":"340000/341700/341721"},{"code":341722,"name":"石台","fullname":"石台县","level":"district","filename":"340000/341700/341722"},{"code":341723,"name":"青阳","fullname":"青阳县","level":"district","filename":"340000/341700/341723"}]},{"code":341800,"name":"宣城","fullname":"宣城市","level":"city","filename":"340000/341800","children":[{"code":341802,"name":"宣州","fullname":"宣州区","level":"district","filename":"340000/341800/341802"},{"code":341821,"name":"郎溪","fullname":"郎溪县","level":"district","filename":"340000/341800/341821"},{"code":341823,"name":"泾县","fullname":"泾县","level":"district","filename":"340000/341800/341823"},{"code":341824,"name":"绩溪","fullname":"绩溪县","level":"district","filename":"340000/341800/341824"},{"code":341825,"name":"旌德","fullname":"旌德县","level":"district","filename":"340000/341800/341825"},{"code":341881,"name":"宁国","fullname":"宁国市","level":"district","filename":"340000/341800/341881"},{"code":341882,"name":"广德","fullname":"广德市","level":"district","filename":"340000/341800/341882"}]}]},{"code":350000,"name":"福建","fullname":"福建省","level":"province","filename":"350000","children":[{"code":350100,"name":"福州","fullname":"福州市","level":"city","filename":"350000/350100","children":[{"code":350102,"name":"鼓楼","fullname":"鼓楼区","level":"district","filename":"350000/350100/350102"},{"code":350103,"name":"台江","fullname":"台江区","level":"district","filename":"350000/350100/350103"},{"code":350104,"name":"仓山","fullname":"仓山区","level":"district","filename":"350000/350100/350104"},{"code":350105,"name":"马尾","fullname":"马尾区","level":"district","filename":"350000/350100/350105"},{"code":350111,"name":"晋安","fullname":"晋安区","level":"district","filename":"350000/350100/350111"},{"code":350112,"name":"长乐","fullname":"长乐区","level":"district","filename":"350000/350100/350112"},{"code":350121,"name":"闽侯","fullname":"闽侯县","level":"district","filename":"350000/350100/350121"},{"code":350122,"name":"连江","fullname":"连江县","level":"district","filename":"350000/350100/350122"},{"code":350123,"name":"罗源","fullname":"罗源县","level":"district","filename":"350000/350100/350123"},{"code":350124,"name":"闽清","fullname":"闽清县","level":"district","filename":"350000/350100/350124"},{"code":350125,"name":"永泰","fullname":"永泰县","level":"district","filename":"350000/350100/350125"},{"code":350128,"name":"平潭","fullname":"平潭县","level":"district","filename":"350000/350100/350128"},{"code":350181,"name":"福清","fullname":"福清市","level":"district","filename":"350000/350100/350181"}]},{"code":350200,"name":"厦门","fullname":"厦门市","level":"city","filename":"350000/350200","children":[{"code":350203,"name":"思明","fullname":"思明区","level":"district","filename":"350000/350200/350203"},{"code":350205,"name":"海沧","fullname":"海沧区","level":"district","filename":"350000/350200/350205"},{"code":350206,"name":"湖里","fullname":"湖里区","level":"district","filename":"350000/350200/350206"},{"code":350211,"name":"集美","fullname":"集美区","level":"district","filename":"350000/350200/350211"},{"code":350212,"name":"同安","fullname":"同安区","level":"district","filename":"350000/350200/350212"},{"code":350213,"name":"翔安","fullname":"翔安区","level":"district","filename":"350000/350200/350213"}]},{"code":350300,"name":"莆田","fullname":"莆田市","level":"city","filename":"350000/350300","children":[{"code":350302,"name":"城厢","fullname":"城厢区","level":"district","filename":"350000/350300/350302"},{"code":350303,"name":"涵江","fullname":"涵江区","level":"district","filename":"350000/350300/350303"},{"code":350304,"name":"荔城","fullname":"荔城区","level":"district","filename":"350000/350300/350304"},{"code":350305,"name":"秀屿","fullname":"秀屿区","level":"district","filename":"350000/350300/350305"},{"code":350322,"name":"仙游","fullname":"仙游县","level":"district","filename":"350000/350300/350322"}]},{"code":350400,"name":"三明","fullname":"三明市","level":"city","filename":"350000/350400","children":[{"code":350403,"name":"三元","fullname":"三元区","level":"district","filename":"350000/350400/350403"},{"code":350421,"name":"明溪","fullname":"明溪县","level":"district","filename":"350000/350400/350421"},{"code":350423,"name":"清流","fullname":"清流县","level":"district","filename":"350000/350400/350423"},{"code":350424,"name":"宁化","fullname":"宁化县","level":"district","filename":"350000/350400/350424"},{"code":350425,"name":"大田","fullname":"大田县","level":"district","filename":"350000/350400/350425"},{"code":350426,"name":"尤溪","fullname":"尤溪县","level":"district","filename":"350000/350400/350426"},{"code":350427,"name":"沙县区","fullname":"沙县区","level":"district","filename":"350000/350400/350427"},{"code":350428,"name":"将乐","fullname":"将乐县","level":"district","filename":"350000/350400/350428"},{"code":350429,"name":"泰宁","fullname":"泰宁县","level":"district","filename":"350000/350400/350429"},{"code":350430,"name":"建宁","fullname":"建宁县","level":"district","filename":"350000/350400/350430"},{"code":350481,"name":"永安","fullname":"永安市","level":"district","filename":"350000/350400/350481"}]},{"code":350500,"name":"泉州","fullname":"泉州市","level":"city","filename":"350000/350500","children":[{"code":350502,"name":"鲤城","fullname":"鲤城区","level":"district","filename":"350000/350500/350502"},{"code":350503,"name":"丰泽","fullname":"丰泽区","level":"district","filename":"350000/350500/350503"},{"code":350504,"name":"洛江","fullname":"洛江区","level":"district","filename":"350000/350500/350504"},{"code":350505,"name":"泉港","fullname":"泉港区","level":"district","filename":"350000/350500/350505"},{"code":350521,"name":"惠安","fullname":"惠安县","level":"district","filename":"350000/350500/350521"},{"code":350524,"name":"安溪","fullname":"安溪县","level":"district","filename":"350000/350500/350524"},{"code":350525,"name":"永春","fullname":"永春县","level":"district","filename":"350000/350500/350525"},{"code":350526,"name":"德化","fullname":"德化县","level":"district","filename":"350000/350500/350526"},{"code":350527,"name":"金门","fullname":"金门县","level":"district","filename":"350000/350500/350527"},{"code":350581,"name":"石狮","fullname":"石狮市","level":"district","filename":"350000/350500/350581"},{"code":350582,"name":"晋江","fullname":"晋江市","level":"district","filename":"350000/350500/350582"},{"code":350583,"name":"南安","fullname":"南安市","level":"district","filename":"350000/350500/350583"}]},{"code":350600,"name":"漳州","fullname":"漳州市","level":"city","filename":"350000/350600","children":[{"code":350602,"name":"芗城","fullname":"芗城区","level":"district","filename":"350000/350600/350602"},{"code":350603,"name":"龙文","fullname":"龙文区","level":"district","filename":"350000/350600/350603"},{"code":350622,"name":"云霄","fullname":"云霄县","level":"district","filename":"350000/350600/350622"},{"code":350623,"name":"漳浦","fullname":"漳浦县","level":"district","filename":"350000/350600/350623"},{"code":350624,"name":"诏安","fullname":"诏安县","level":"district","filename":"350000/350600/350624"},{"code":350625,"name":"长泰","fullname":"长泰区","level":"district","filename":"350000/350600/350625"},{"code":350626,"name":"东山","fullname":"东山县","level":"district","filename":"350000/350600/350626"},{"code":350627,"name":"南靖","fullname":"南靖县","level":"district","filename":"350000/350600/350627"},{"code":350628,"name":"平和","fullname":"平和县","level":"district","filename":"350000/350600/350628"},{"code":350629,"name":"华安","fullname":"华安县","level":"district","filename":"350000/350600/350629"},{"code":350681,"name":"龙海","fullname":"龙海区","level":"district","filename":"350000/350600/350681"}]},{"code":350700,"name":"南平","fullname":"南平市","level":"city","filename":"350000/350700","children":[{"code":350702,"name":"延平","fullname":"延平区","level":"district","filename":"350000/350700/350702"},{"code":350703,"name":"建阳","fullname":"建阳区","level":"district","filename":"350000/350700/350703"},{"code":350721,"name":"顺昌","fullname":"顺昌县","level":"district","filename":"350000/350700/350721"},{"code":350722,"name":"浦城","fullname":"浦城县","level":"district","filename":"350000/350700/350722"},{"code":350723,"name":"光泽","fullname":"光泽县","level":"district","filename":"350000/350700/350723"},{"code":350724,"name":"松溪","fullname":"松溪县","level":"district","filename":"350000/350700/350724"},{"code":350725,"name":"政和","fullname":"政和县","level":"district","filename":"350000/350700/350725"},{"code":350781,"name":"邵武","fullname":"邵武市","level":"district","filename":"350000/350700/350781"},{"code":350782,"name":"武夷山","fullname":"武夷山市","level":"district","filename":"350000/350700/350782"},{"code":350783,"name":"建瓯","fullname":"建瓯市","level":"district","filename":"350000/350700/350783"}]},{"code":350800,"name":"龙岩","fullname":"龙岩市","level":"city","filename":"350000/350800","children":[{"code":350802,"name":"新罗","fullname":"新罗区","level":"district","filename":"350000/350800/350802"},{"code":350803,"name":"永定","fullname":"永定区","level":"district","filename":"350000/350800/350803"},{"code":350821,"name":"长汀","fullname":"长汀县","level":"district","filename":"350000/350800/350821"},{"code":350823,"name":"上杭","fullname":"上杭县","level":"district","filename":"350000/350800/350823"},{"code":350824,"name":"武平","fullname":"武平县","level":"district","filename":"350000/350800/350824"},{"code":350825,"name":"连城","fullname":"连城县","level":"district","filename":"350000/350800/350825"},{"code":350881,"name":"漳平","fullname":"漳平市","level":"district","filename":"350000/350800/350881"}]},{"code":350900,"name":"宁德","fullname":"宁德市","level":"city","filename":"350000/350900","children":[{"code":350902,"name":"蕉城","fullname":"蕉城区","level":"district","filename":"350000/350900/350902"},{"code":350921,"name":"霞浦","fullname":"霞浦县","level":"district","filename":"350000/350900/350921"},{"code":350922,"name":"古田","fullname":"古田县","level":"district","filename":"350000/350900/350922"},{"code":350923,"name":"屏南","fullname":"屏南县","level":"district","filename":"350000/350900/350923"},{"code":350924,"name":"寿宁","fullname":"寿宁县","level":"district","filename":"350000/350900/350924"},{"code":350925,"name":"周宁","fullname":"周宁县","level":"district","filename":"350000/350900/350925"},{"code":350926,"name":"柘荣","fullname":"柘荣县","level":"district","filename":"350000/350900/350926"},{"code":350981,"name":"福安","fullname":"福安市","level":"district","filename":"350000/350900/350981"},{"code":350982,"name":"福鼎","fullname":"福鼎市","level":"district","filename":"350000/350900/350982"}]}]},{"code":360000,"name":"江西","fullname":"江西省","level":"province","filename":"360000","children":[{"code":360100,"name":"南昌","fullname":"南昌市","level":"city","filename":"360000/360100","children":[{"code":360102,"name":"东湖","fullname":"东湖区","level":"district","filename":"360000/360100/360102"},{"code":360103,"name":"西湖","fullname":"西湖区","level":"district","filename":"360000/360100/360103"},{"code":360104,"name":"青云谱","fullname":"青云谱区","level":"district","filename":"360000/360100/360104"},{"code":360111,"name":"青山湖","fullname":"青山湖区","level":"district","filename":"360000/360100/360111"},{"code":360112,"name":"新建","fullname":"新建区","level":"district","filename":"360000/360100/360112"},{"code":360113,"name":"红谷滩","fullname":"红谷滩区","level":"district","filename":"360000/360100/360113"},{"code":360121,"name":"南昌","fullname":"南昌县","level":"district","filename":"360000/360100/360121"},{"code":360123,"name":"安义","fullname":"安义县","level":"district","filename":"360000/360100/360123"},{"code":360124,"name":"进贤","fullname":"进贤县","level":"district","filename":"360000/360100/360124"}]},{"code":360200,"name":"景德镇","fullname":"景德镇市","level":"city","filename":"360000/360200","children":[{"code":360202,"name":"昌江","fullname":"昌江区","level":"district","filename":"360000/360200/360202"},{"code":360203,"name":"珠山","fullname":"珠山区","level":"district","filename":"360000/360200/360203"},{"code":360222,"name":"浮梁","fullname":"浮梁县","level":"district","filename":"360000/360200/360222"},{"code":360281,"name":"乐平","fullname":"乐平市","level":"district","filename":"360000/360200/360281"}]},{"code":360300,"name":"萍乡","fullname":"萍乡市","level":"city","filename":"360000/360300","children":[{"code":360302,"name":"安源","fullname":"安源区","level":"district","filename":"360000/360300/360302"},{"code":360313,"name":"湘东","fullname":"湘东区","level":"district","filename":"360000/360300/360313"},{"code":360321,"name":"莲花","fullname":"莲花县","level":"district","filename":"360000/360300/360321"},{"code":360322,"name":"上栗","fullname":"上栗县","level":"district","filename":"360000/360300/360322"},{"code":360323,"name":"芦溪","fullname":"芦溪县","level":"district","filename":"360000/360300/360323"}]},{"code":360400,"name":"九江","fullname":"九江市","level":"city","filename":"360000/360400","children":[{"code":360402,"name":"濂溪","fullname":"濂溪区","level":"district","filename":"360000/360400/360402"},{"code":360403,"name":"浔阳","fullname":"浔阳区","level":"district","filename":"360000/360400/360403"},{"code":360404,"name":"柴桑","fullname":"柴桑区","level":"district","filename":"360000/360400/360404"},{"code":360423,"name":"武宁","fullname":"武宁县","level":"district","filename":"360000/360400/360423"},{"code":360424,"name":"修水","fullname":"修水县","level":"district","filename":"360000/360400/360424"},{"code":360425,"name":"永修","fullname":"永修县","level":"district","filename":"360000/360400/360425"},{"code":360426,"name":"德安","fullname":"德安县","level":"district","filename":"360000/360400/360426"},{"code":360428,"name":"都昌","fullname":"都昌县","level":"district","filename":"360000/360400/360428"},{"code":360429,"name":"湖口","fullname":"湖口县","level":"district","filename":"360000/360400/360429"},{"code":360430,"name":"彭泽","fullname":"彭泽县","level":"district","filename":"360000/360400/360430"},{"code":360481,"name":"瑞昌","fullname":"瑞昌市","level":"district","filename":"360000/360400/360481"},{"code":360482,"name":"共青城","fullname":"共青城市","level":"district","filename":"360000/360400/360482"},{"code":360483,"name":"庐山","fullname":"庐山市","level":"district","filename":"360000/360400/360483"}]},{"code":360500,"name":"新余","fullname":"新余市","level":"city","filename":"360000/360500","children":[{"code":360502,"name":"渝水","fullname":"渝水区","level":"district","filename":"360000/360500/360502"},{"code":360521,"name":"分宜","fullname":"分宜县","level":"district","filename":"360000/360500/360521"}]},{"code":360600,"name":"鹰潭","fullname":"鹰潭市","level":"city","filename":"360000/360600","children":[{"code":360602,"name":"月湖","fullname":"月湖区","level":"district","filename":"360000/360600/360602"},{"code":360603,"name":"余江","fullname":"余江区","level":"district","filename":"360000/360600/360603"},{"code":360681,"name":"贵溪","fullname":"贵溪市","level":"district","filename":"360000/360600/360681"}]},{"code":360700,"name":"赣州","fullname":"赣州市","level":"city","filename":"360000/360700","children":[{"code":360702,"name":"章贡","fullname":"章贡区","level":"district","filename":"360000/360700/360702"},{"code":360703,"name":"南康","fullname":"南康区","level":"district","filename":"360000/360700/360703"},{"code":360704,"name":"赣县区","fullname":"赣县区","level":"district","filename":"360000/360700/360704"},{"code":360722,"name":"信丰","fullname":"信丰县","level":"district","filename":"360000/360700/360722"},{"code":360723,"name":"大余","fullname":"大余县","level":"district","filename":"360000/360700/360723"},{"code":360724,"name":"上犹","fullname":"上犹县","level":"district","filename":"360000/360700/360724"},{"code":360725,"name":"崇义","fullname":"崇义县","level":"district","filename":"360000/360700/360725"},{"code":360726,"name":"安远","fullname":"安远县","level":"district","filename":"360000/360700/360726"},{"code":360728,"name":"定南","fullname":"定南县","level":"district","filename":"360000/360700/360728"},{"code":360729,"name":"全南","fullname":"全南县","level":"district","filename":"360000/360700/360729"},{"code":360730,"name":"宁都","fullname":"宁都县","level":"district","filename":"360000/360700/360730"},{"code":360731,"name":"于都","fullname":"于都县","level":"district","filename":"360000/360700/360731"},{"code":360732,"name":"兴国","fullname":"兴国县","level":"district","filename":"360000/360700/360732"},{"code":360733,"name":"会昌","fullname":"会昌县","level":"district","filename":"360000/360700/360733"},{"code":360734,"name":"寻乌","fullname":"寻乌县","level":"district","filename":"360000/360700/360734"},{"code":360735,"name":"石城","fullname":"石城县","level":"district","filename":"360000/360700/360735"},{"code":360781,"name":"瑞金","fullname":"瑞金市","level":"district","filename":"360000/360700/360781"},{"code":360783,"name":"龙南","fullname":"龙南市","level":"district","filename":"360000/360700/360783"}]},{"code":360800,"name":"吉安","fullname":"吉安市","level":"city","filename":"360000/360800","children":[{"code":360802,"name":"吉州","fullname":"吉州区","level":"district","filename":"360000/360800/360802"},{"code":360803,"name":"青原","fullname":"青原区","level":"district","filename":"360000/360800/360803"},{"code":360821,"name":"吉安","fullname":"吉安县","level":"district","filename":"360000/360800/360821"},{"code":360822,"name":"吉水","fullname":"吉水县","level":"district","filename":"360000/360800/360822"},{"code":360823,"name":"峡江","fullname":"峡江县","level":"district","filename":"360000/360800/360823"},{"code":360824,"name":"新干","fullname":"新干县","level":"district","filename":"360000/360800/360824"},{"code":360825,"name":"永丰","fullname":"永丰县","level":"district","filename":"360000/360800/360825"},{"code":360826,"name":"泰和","fullname":"泰和县","level":"district","filename":"360000/360800/360826"},{"code":360827,"name":"遂川","fullname":"遂川县","level":"district","filename":"360000/360800/360827"},{"code":360828,"name":"万安","fullname":"万安县","level":"district","filename":"360000/360800/360828"},{"code":360829,"name":"安福","fullname":"安福县","level":"district","filename":"360000/360800/360829"},{"code":360830,"name":"永新","fullname":"永新县","level":"district","filename":"360000/360800/360830"},{"code":360881,"name":"井冈山","fullname":"井冈山市","level":"district","filename":"360000/360800/360881"}]},{"code":360900,"name":"宜春","fullname":"宜春市","level":"city","filename":"360000/360900","children":[{"code":360902,"name":"袁州","fullname":"袁州区","level":"district","filename":"360000/360900/360902"},{"code":360921,"name":"奉新","fullname":"奉新县","level":"district","filename":"360000/360900/360921"},{"code":360922,"name":"万载","fullname":"万载县","level":"district","filename":"360000/360900/360922"},{"code":360923,"name":"上高","fullname":"上高县","level":"district","filename":"360000/360900/360923"},{"code":360924,"name":"宜丰","fullname":"宜丰县","level":"district","filename":"360000/360900/360924"},{"code":360925,"name":"靖安","fullname":"靖安县","level":"district","filename":"360000/360900/360925"},{"code":360926,"name":"铜鼓","fullname":"铜鼓县","level":"district","filename":"360000/360900/360926"},{"code":360981,"name":"丰城","fullname":"丰城市","level":"district","filename":"360000/360900/360981"},{"code":360982,"name":"樟树","fullname":"樟树市","level":"district","filename":"360000/360900/360982"},{"code":360983,"name":"高安","fullname":"高安市","level":"district","filename":"360000/360900/360983"}]},{"code":361000,"name":"抚州","fullname":"抚州市","level":"city","filename":"360000/361000","children":[{"code":361002,"name":"临川","fullname":"临川区","level":"district","filename":"360000/361000/361002"},{"code":361003,"name":"东乡","fullname":"东乡区","level":"district","filename":"360000/361000/361003"},{"code":361021,"name":"南城","fullname":"南城县","level":"district","filename":"360000/361000/361021"},{"code":361022,"name":"黎川","fullname":"黎川县","level":"district","filename":"360000/361000/361022"},{"code":361023,"name":"南丰","fullname":"南丰县","level":"district","filename":"360000/361000/361023"},{"code":361024,"name":"崇仁","fullname":"崇仁县","level":"district","filename":"360000/361000/361024"},{"code":361025,"name":"乐安","fullname":"乐安县","level":"district","filename":"360000/361000/361025"},{"code":361026,"name":"宜黄","fullname":"宜黄县","level":"district","filename":"360000/361000/361026"},{"code":361027,"name":"金溪","fullname":"金溪县","level":"district","filename":"360000/361000/361027"},{"code":361028,"name":"资溪","fullname":"资溪县","level":"district","filename":"360000/361000/361028"},{"code":361030,"name":"广昌","fullname":"广昌县","level":"district","filename":"360000/361000/361030"}]},{"code":361100,"name":"上饶","fullname":"上饶市","level":"city","filename":"360000/361100","children":[{"code":361102,"name":"信州","fullname":"信州区","level":"district","filename":"360000/361100/361102"},{"code":361103,"name":"广丰","fullname":"广丰区","level":"district","filename":"360000/361100/361103"},{"code":361104,"name":"广信","fullname":"广信区","level":"district","filename":"360000/361100/361104"},{"code":361123,"name":"玉山","fullname":"玉山县","level":"district","filename":"360000/361100/361123"},{"code":361124,"name":"铅山","fullname":"铅山县","level":"district","filename":"360000/361100/361124"},{"code":361125,"name":"横峰","fullname":"横峰县","level":"district","filename":"360000/361100/361125"},{"code":361126,"name":"弋阳","fullname":"弋阳县","level":"district","filename":"360000/361100/361126"},{"code":361127,"name":"余干","fullname":"余干县","level":"district","filename":"360000/361100/361127"},{"code":361128,"name":"鄱阳","fullname":"鄱阳县","level":"district","filename":"360000/361100/361128"},{"code":361129,"name":"万年","fullname":"万年县","level":"district","filename":"360000/361100/361129"},{"code":361130,"name":"婺源","fullname":"婺源县","level":"district","filename":"360000/361100/361130"},{"code":361181,"name":"德兴","fullname":"德兴市","level":"district","filename":"360000/361100/361181"}]}]},{"code":370000,"name":"山东","fullname":"山东省","level":"province","filename":"370000","children":[{"code":370100,"name":"济南","fullname":"济南市","level":"city","filename":"370000/370100","children":[{"code":370102,"name":"历下","fullname":"历下区","level":"district","filename":"370000/370100/370102"},{"code":370103,"name":"市中区","fullname":"市中区","level":"district","filename":"370000/370100/370103"},{"code":370104,"name":"槐荫","fullname":"槐荫区","level":"district","filename":"370000/370100/370104"},{"code":370105,"name":"天桥","fullname":"天桥区","level":"district","filename":"370000/370100/370105"},{"code":370112,"name":"历城","fullname":"历城区","level":"district","filename":"370000/370100/370112"},{"code":370113,"name":"长清","fullname":"长清区","level":"district","filename":"370000/370100/370113"},{"code":370114,"name":"章丘","fullname":"章丘区","level":"district","filename":"370000/370100/370114"},{"code":370115,"name":"济阳","fullname":"济阳区","level":"district","filename":"370000/370100/370115"},{"code":370116,"name":"莱芜","fullname":"莱芜区","level":"district","filename":"370000/370100/370116"},{"code":370117,"name":"钢城","fullname":"钢城区","level":"district","filename":"370000/370100/370117"},{"code":370124,"name":"平阴","fullname":"平阴县","level":"district","filename":"370000/370100/370124"},{"code":370126,"name":"商河","fullname":"商河县","level":"district","filename":"370000/370100/370126"}]},{"code":370200,"name":"青岛","fullname":"青岛市","level":"city","filename":"370000/370200","children":[{"code":370202,"name":"市南区","fullname":"市南区","level":"district","filename":"370000/370200/370202"},{"code":370203,"name":"市北区","fullname":"市北区","level":"district","filename":"370000/370200/370203"},{"code":370211,"name":"黄岛","fullname":"黄岛区","level":"district","filename":"370000/370200/370211"},{"code":370212,"name":"崂山","fullname":"崂山区","level":"district","filename":"370000/370200/370212"},{"code":370213,"name":"李沧","fullname":"李沧区","level":"district","filename":"370000/370200/370213"},{"code":370214,"name":"城阳","fullname":"城阳区","level":"district","filename":"370000/370200/370214"},{"code":370215,"name":"即墨","fullname":"即墨区","level":"district","filename":"370000/370200/370215"},{"code":370281,"name":"胶州","fullname":"胶州市","level":"district","filename":"370000/370200/370281"},{"code":370283,"name":"平度","fullname":"平度市","level":"district","filename":"370000/370200/370283"},{"code":370285,"name":"莱西","fullname":"莱西市","level":"district","filename":"370000/370200/370285"}]},{"code":370300,"name":"淄博","fullname":"淄博市","level":"city","filename":"370000/370300","children":[{"code":370302,"name":"淄川","fullname":"淄川区","level":"district","filename":"370000/370300/370302"},{"code":370303,"name":"张店","fullname":"张店区","level":"district","filename":"370000/370300/370303"},{"code":370304,"name":"博山","fullname":"博山区","level":"district","filename":"370000/370300/370304"},{"code":370305,"name":"临淄","fullname":"临淄区","level":"district","filename":"370000/370300/370305"},{"code":370306,"name":"周村","fullname":"周村区","level":"district","filename":"370000/370300/370306"},{"code":370321,"name":"桓台","fullname":"桓台县","level":"district","filename":"370000/370300/370321"},{"code":370322,"name":"高青","fullname":"高青县","level":"district","filename":"370000/370300/370322"},{"code":370323,"name":"沂源","fullname":"沂源县","level":"district","filename":"370000/370300/370323"}]},{"code":370400,"name":"枣庄","fullname":"枣庄市","level":"city","filename":"370000/370400","children":[{"code":370402,"name":"市中区","fullname":"市中区","level":"district","filename":"370000/370400/370402"},{"code":370403,"name":"薛城","fullname":"薛城区","level":"district","filename":"370000/370400/370403"},{"code":370404,"name":"峄城","fullname":"峄城区","level":"district","filename":"370000/370400/370404"},{"code":370405,"name":"台儿庄","fullname":"台儿庄区","level":"district","filename":"370000/370400/370405"},{"code":370406,"name":"山亭","fullname":"山亭区","level":"district","filename":"370000/370400/370406"},{"code":370481,"name":"滕州","fullname":"滕州市","level":"district","filename":"370000/370400/370481"}]},{"code":370500,"name":"东营","fullname":"东营市","level":"city","filename":"370000/370500","children":[{"code":370502,"name":"东营","fullname":"东营区","level":"district","filename":"370000/370500/370502"},{"code":370503,"name":"河口","fullname":"河口区","level":"district","filename":"370000/370500/370503"},{"code":370505,"name":"垦利","fullname":"垦利区","level":"district","filename":"370000/370500/370505"},{"code":370522,"name":"利津","fullname":"利津县","level":"district","filename":"370000/370500/370522"},{"code":370523,"name":"广饶","fullname":"广饶县","level":"district","filename":"370000/370500/370523"}]},{"code":370600,"name":"烟台","fullname":"烟台市","level":"city","filename":"370000/370600","children":[{"code":370602,"name":"芝罘","fullname":"芝罘区","level":"district","filename":"370000/370600/370602"},{"code":370611,"name":"福山","fullname":"福山区","level":"district","filename":"370000/370600/370611"},{"code":370612,"name":"牟平","fullname":"牟平区","level":"district","filename":"370000/370600/370612"},{"code":370613,"name":"莱山","fullname":"莱山区","level":"district","filename":"370000/370600/370613"},{"code":370614,"name":"蓬莱","fullname":"蓬莱区","level":"district","filename":"370000/370600/370614"},{"code":370681,"name":"龙口","fullname":"龙口市","level":"district","filename":"370000/370600/370681"},{"code":370682,"name":"莱阳","fullname":"莱阳市","level":"district","filename":"370000/370600/370682"},{"code":370683,"name":"莱州","fullname":"莱州市","level":"district","filename":"370000/370600/370683"},{"code":370685,"name":"招远","fullname":"招远市","level":"district","filename":"370000/370600/370685"},{"code":370686,"name":"栖霞","fullname":"栖霞市","level":"district","filename":"370000/370600/370686"},{"code":370687,"name":"海阳","fullname":"海阳市","level":"district","filename":"370000/370600/370687"}]},{"code":370700,"name":"潍坊","fullname":"潍坊市","level":"city","filename":"370000/370700","children":[{"code":370702,"name":"潍城","fullname":"潍城区","level":"district","filename":"370000/370700/370702"},{"code":370703,"name":"寒亭","fullname":"寒亭区","level":"district","filename":"370000/370700/370703"},{"code":370704,"name":"坊子","fullname":"坊子区","level":"district","filename":"370000/370700/370704"},{"code":370705,"name":"奎文","fullname":"奎文区","level":"district","filename":"370000/370700/370705"},{"code":370724,"name":"临朐","fullname":"临朐县","level":"district","filename":"370000/370700/370724"},{"code":370725,"name":"昌乐","fullname":"昌乐县","level":"district","filename":"370000/370700/370725"},{"code":370781,"name":"青州","fullname":"青州市","level":"district","filename":"370000/370700/370781"},{"code":370782,"name":"诸城","fullname":"诸城市","level":"district","filename":"370000/370700/370782"},{"code":370783,"name":"寿光","fullname":"寿光市","level":"district","filename":"370000/370700/370783"},{"code":370784,"name":"安丘","fullname":"安丘市","level":"district","filename":"370000/370700/370784"},{"code":370785,"name":"高密","fullname":"高密市","level":"district","filename":"370000/370700/370785"},{"code":370786,"name":"昌邑","fullname":"昌邑市","level":"district","filename":"370000/370700/370786"}]},{"code":370800,"name":"济宁","fullname":"济宁市","level":"city","filename":"370000/370800","children":[{"code":370811,"name":"任城","fullname":"任城区","level":"district","filename":"370000/370800/370811"},{"code":370812,"name":"兖州","fullname":"兖州区","level":"district","filename":"370000/370800/370812"},{"code":370826,"name":"微山","fullname":"微山县","level":"district","filename":"370000/370800/370826"},{"code":370827,"name":"鱼台","fullname":"鱼台县","level":"district","filename":"370000/370800/370827"},{"code":370828,"name":"金乡","fullname":"金乡县","level":"district","filename":"370000/370800/370828"},{"code":370829,"name":"嘉祥","fullname":"嘉祥县","level":"district","filename":"370000/370800/370829"},{"code":370830,"name":"汶上","fullname":"汶上县","level":"district","filename":"370000/370800/370830"},{"code":370831,"name":"泗水","fullname":"泗水县","level":"district","filename":"370000/370800/370831"},{"code":370832,"name":"梁山","fullname":"梁山县","level":"district","filename":"370000/370800/370832"},{"code":370881,"name":"曲阜","fullname":"曲阜市","level":"district","filename":"370000/370800/370881"},{"code":370883,"name":"邹城","fullname":"邹城市","level":"district","filename":"370000/370800/370883"}]},{"code":370900,"name":"泰安","fullname":"泰安市","level":"city","filename":"370000/370900","children":[{"code":370902,"name":"泰山","fullname":"泰山区","level":"district","filename":"370000/370900/370902"},{"code":370911,"name":"岱岳","fullname":"岱岳区","level":"district","filename":"370000/370900/370911"},{"code":370921,"name":"宁阳","fullname":"宁阳县","level":"district","filename":"370000/370900/370921"},{"code":370923,"name":"东平","fullname":"东平县","level":"district","filename":"370000/370900/370923"},{"code":370982,"name":"新泰","fullname":"新泰市","level":"district","filename":"370000/370900/370982"},{"code":370983,"name":"肥城","fullname":"肥城市","level":"district","filename":"370000/370900/370983"}]},{"code":371000,"name":"威海","fullname":"威海市","level":"city","filename":"370000/371000","children":[{"code":371002,"name":"环翠","fullname":"环翠区","level":"district","filename":"370000/371000/371002"},{"code":371003,"name":"文登","fullname":"文登区","level":"district","filename":"370000/371000/371003"},{"code":371082,"name":"荣成","fullname":"荣成市","level":"district","filename":"370000/371000/371082"},{"code":371083,"name":"乳山","fullname":"乳山市","level":"district","filename":"370000/371000/371083"}]},{"code":371100,"name":"日照","fullname":"日照市","level":"city","filename":"370000/371100","children":[{"code":371102,"name":"东港","fullname":"东港区","level":"district","filename":"370000/371100/371102"},{"code":371103,"name":"岚山","fullname":"岚山区","level":"district","filename":"370000/371100/371103"},{"code":371121,"name":"五莲","fullname":"五莲县","level":"district","filename":"370000/371100/371121"},{"code":371122,"name":"莒县","fullname":"莒县","level":"district","filename":"370000/371100/371122"}]},{"code":371300,"name":"临沂","fullname":"临沂市","level":"city","filename":"370000/371300","children":[{"code":371302,"name":"兰山","fullname":"兰山区","level":"district","filename":"370000/371300/371302"},{"code":371311,"name":"罗庄","fullname":"罗庄区","level":"district","filename":"370000/371300/371311"},{"code":371312,"name":"河东","fullname":"河东区","level":"district","filename":"370000/371300/371312"},{"code":371321,"name":"沂南","fullname":"沂南县","level":"district","filename":"370000/371300/371321"},{"code":371322,"name":"郯城","fullname":"郯城县","level":"district","filename":"370000/371300/371322"},{"code":371323,"name":"沂水","fullname":"沂水县","level":"district","filename":"370000/371300/371323"},{"code":371324,"name":"兰陵","fullname":"兰陵县","level":"district","filename":"370000/371300/371324"},{"code":371325,"name":"费县","fullname":"费县","level":"district","filename":"370000/371300/371325"},{"code":371326,"name":"平邑","fullname":"平邑县","level":"district","filename":"370000/371300/371326"},{"code":371327,"name":"莒南","fullname":"莒南县","level":"district","filename":"370000/371300/371327"},{"code":371328,"name":"蒙阴","fullname":"蒙阴县","level":"district","filename":"370000/371300/371328"},{"code":371329,"name":"临沭","fullname":"临沭县","level":"district","filename":"370000/371300/371329"}]},{"code":371400,"name":"德州","fullname":"德州市","level":"city","filename":"370000/371400","children":[{"code":371402,"name":"德城","fullname":"德城区","level":"district","filename":"370000/371400/371402"},{"code":371403,"name":"陵城","fullname":"陵城区","level":"district","filename":"370000/371400/371403"},{"code":371422,"name":"宁津","fullname":"宁津县","level":"district","filename":"370000/371400/371422"},{"code":371423,"name":"庆云","fullname":"庆云县","level":"district","filename":"370000/371400/371423"},{"code":371424,"name":"临邑","fullname":"临邑县","level":"district","filename":"370000/371400/371424"},{"code":371425,"name":"齐河","fullname":"齐河县","level":"district","filename":"370000/371400/371425"},{"code":371426,"name":"平原","fullname":"平原县","level":"district","filename":"370000/371400/371426"},{"code":371427,"name":"夏津","fullname":"夏津县","level":"district","filename":"370000/371400/371427"},{"code":371428,"name":"武城","fullname":"武城县","level":"district","filename":"370000/371400/371428"},{"code":371481,"name":"乐陵","fullname":"乐陵市","level":"district","filename":"370000/371400/371481"},{"code":371482,"name":"禹城","fullname":"禹城市","level":"district","filename":"370000/371400/371482"}]},{"code":371500,"name":"聊城","fullname":"聊城市","level":"city","filename":"370000/371500","children":[{"code":371502,"name":"东昌府","fullname":"东昌府区","level":"district","filename":"370000/371500/371502"},{"code":371503,"name":"茌平","fullname":"茌平区","level":"district","filename":"370000/371500/371503"},{"code":371521,"name":"阳谷","fullname":"阳谷县","level":"district","filename":"370000/371500/371521"},{"code":371522,"name":"莘县","fullname":"莘县","level":"district","filename":"370000/371500/371522"},{"code":371524,"name":"东阿","fullname":"东阿县","level":"district","filename":"370000/371500/371524"},{"code":371525,"name":"冠县","fullname":"冠县","level":"district","filename":"370000/371500/371525"},{"code":371526,"name":"高唐","fullname":"高唐县","level":"district","filename":"370000/371500/371526"},{"code":371581,"name":"临清","fullname":"临清市","level":"district","filename":"370000/371500/371581"}]},{"code":371600,"name":"滨州","fullname":"滨州市","level":"city","filename":"370000/371600","children":[{"code":371602,"name":"滨城","fullname":"滨城区","level":"district","filename":"370000/371600/371602"},{"code":371603,"name":"沾化","fullname":"沾化区","level":"district","filename":"370000/371600/371603"},{"code":371621,"name":"惠民","fullname":"惠民县","level":"district","filename":"370000/371600/371621"},{"code":371622,"name":"阳信","fullname":"阳信县","level":"district","filename":"370000/371600/371622"},{"code":371623,"name":"无棣","fullname":"无棣县","level":"district","filename":"370000/371600/371623"},{"code":371625,"name":"博兴","fullname":"博兴县","level":"district","filename":"370000/371600/371625"},{"code":371681,"name":"邹平","fullname":"邹平市","level":"district","filename":"370000/371600/371681"}]},{"code":371700,"name":"菏泽","fullname":"菏泽市","level":"city","filename":"370000/371700","children":[{"code":371702,"name":"牡丹","fullname":"牡丹区","level":"district","filename":"370000/371700/371702"},{"code":371703,"name":"定陶","fullname":"定陶区","level":"district","filename":"370000/371700/371703"},{"code":371721,"name":"曹县","fullname":"曹县","level":"district","filename":"370000/371700/371721"},{"code":371722,"name":"单县","fullname":"单县","level":"district","filename":"370000/371700/371722"},{"code":371723,"name":"成武","fullname":"成武县","level":"district","filename":"370000/371700/371723"},{"code":371724,"name":"巨野","fullname":"巨野县","level":"district","filename":"370000/371700/371724"},{"code":371725,"name":"郓城","fullname":"郓城县","level":"district","filename":"370000/371700/371725"},{"code":371726,"name":"鄄城","fullname":"鄄城县","level":"district","filename":"370000/371700/371726"},{"code":371728,"name":"东明","fullname":"东明县","level":"district","filename":"370000/371700/371728"}]}]},{"code":410000,"name":"河南","fullname":"河南省","level":"province","filename":"410000","children":[{"code":410100,"name":"郑州","fullname":"郑州市","level":"city","filename":"410000/410100","children":[{"code":410102,"name":"中原","fullname":"中原区","level":"district","filename":"410000/410100/410102"},{"code":410103,"name":"二七","fullname":"二七区","level":"district","filename":"410000/410100/410103"},{"code":410104,"name":"管城","fullname":"管城回族区","level":"district","filename":"410000/410100/410104"},{"code":410105,"name":"金水","fullname":"金水区","level":"district","filename":"410000/410100/410105"},{"code":410106,"name":"上街","fullname":"上街区","level":"district","filename":"410000/410100/410106"},{"code":410108,"name":"惠济","fullname":"惠济区","level":"district","filename":"410000/410100/410108"},{"code":410122,"name":"中牟","fullname":"中牟县","level":"district","filename":"410000/410100/410122"},{"code":410181,"name":"巩义","fullname":"巩义市","level":"district","filename":"410000/410100/410181"},{"code":410182,"name":"荥阳","fullname":"荥阳市","level":"district","filename":"410000/410100/410182"},{"code":410183,"name":"新密","fullname":"新密市","level":"district","filename":"410000/410100/410183"},{"code":410184,"name":"新郑","fullname":"新郑市","level":"district","filename":"410000/410100/410184"},{"code":410185,"name":"登封","fullname":"登封市","level":"district","filename":"410000/410100/410185"}]},{"code":410200,"name":"开封","fullname":"开封市","level":"city","filename":"410000/410200","children":[{"code":410202,"name":"龙亭","fullname":"龙亭区","level":"district","filename":"410000/410200/410202"},{"code":410203,"name":"顺河","fullname":"顺河回族区","level":"district","filename":"410000/410200/410203"},{"code":410204,"name":"鼓楼","fullname":"鼓楼区","level":"district","filename":"410000/410200/410204"},{"code":410205,"name":"禹王台","fullname":"禹王台区","level":"district","filename":"410000/410200/410205"},{"code":410212,"name":"祥符","fullname":"祥符区","level":"district","filename":"410000/410200/410212"},{"code":410221,"name":"杞县","fullname":"杞县","level":"district","filename":"410000/410200/410221"},{"code":410222,"name":"通许","fullname":"通许县","level":"district","filename":"410000/410200/410222"},{"code":410223,"name":"尉氏","fullname":"尉氏县","level":"district","filename":"410000/410200/410223"},{"code":410225,"name":"兰考","fullname":"兰考县","level":"district","filename":"410000/410200/410225"}]},{"code":410300,"name":"洛阳","fullname":"洛阳市","level":"city","filename":"410000/410300","children":[{"code":410302,"name":"老城","fullname":"老城区","level":"district","filename":"410000/410300/410302"},{"code":410303,"name":"西工","fullname":"西工区","level":"district","filename":"410000/410300/410303"},{"code":410304,"name":"瀍河","fullname":"瀍河回族区","level":"district","filename":"410000/410300/410304"},{"code":410305,"name":"涧西","fullname":"涧西区","level":"district","filename":"410000/410300/410305"},{"code":410307,"name":"偃师","fullname":"偃师区","level":"district","filename":"410000/410300/410307"},{"code":410308,"name":"孟津","fullname":"孟津区","level":"district","filename":"410000/410300/410308"},{"code":410311,"name":"洛龙","fullname":"洛龙区","level":"district","filename":"410000/410300/410311"},{"code":410323,"name":"新安","fullname":"新安县","level":"district","filename":"410000/410300/410323"},{"code":410324,"name":"栾川","fullname":"栾川县","level":"district","filename":"410000/410300/410324"},{"code":410325,"name":"嵩县","fullname":"嵩县","level":"district","filename":"410000/410300/410325"},{"code":410326,"name":"汝阳","fullname":"汝阳县","level":"district","filename":"410000/410300/410326"},{"code":410327,"name":"宜阳","fullname":"宜阳县","level":"district","filename":"410000/410300/410327"},{"code":410328,"name":"洛宁","fullname":"洛宁县","level":"district","filename":"410000/410300/410328"},{"code":410329,"name":"伊川","fullname":"伊川县","level":"district","filename":"410000/410300/410329"}]},{"code":410400,"name":"平顶山","fullname":"平顶山市","level":"city","filename":"410000/410400","children":[{"code":410402,"name":"新华","fullname":"新华区","level":"district","filename":"410000/410400/410402"},{"code":410403,"name":"卫东","fullname":"卫东区","level":"district","filename":"410000/410400/410403"},{"code":410404,"name":"石龙","fullname":"石龙区","level":"district","filename":"410000/410400/410404"},{"code":410411,"name":"湛河","fullname":"湛河区","level":"district","filename":"410000/410400/410411"},{"code":410421,"name":"宝丰","fullname":"宝丰县","level":"district","filename":"410000/410400/410421"},{"code":410422,"name":"叶县","fullname":"叶县","level":"district","filename":"410000/410400/410422"},{"code":410423,"name":"鲁山","fullname":"鲁山县","level":"district","filename":"410000/410400/410423"},{"code":410425,"name":"郏县","fullname":"郏县","level":"district","filename":"410000/410400/410425"},{"code":410481,"name":"舞钢","fullname":"舞钢市","level":"district","filename":"410000/410400/410481"},{"code":410482,"name":"汝州","fullname":"汝州市","level":"district","filename":"410000/410400/410482"}]},{"code":410500,"name":"安阳","fullname":"安阳市","level":"city","filename":"410000/410500","children":[{"code":410502,"name":"文峰","fullname":"文峰区","level":"district","filename":"410000/410500/410502"},{"code":410503,"name":"北关","fullname":"北关区","level":"district","filename":"410000/410500/410503"},{"code":410505,"name":"殷都","fullname":"殷都区","level":"district","filename":"410000/410500/410505"},{"code":410506,"name":"龙安","fullname":"龙安区","level":"district","filename":"410000/410500/410506"},{"code":410522,"name":"安阳","fullname":"安阳县","level":"district","filename":"410000/410500/410522"},{"code":410523,"name":"汤阴","fullname":"汤阴县","level":"district","filename":"410000/410500/410523"},{"code":410526,"name":"滑县","fullname":"滑县","level":"district","filename":"410000/410500/410526"},{"code":410527,"name":"内黄","fullname":"内黄县","level":"district","filename":"410000/410500/410527"},{"code":410581,"name":"林州","fullname":"林州市","level":"district","filename":"410000/410500/410581"}]},{"code":410600,"name":"鹤壁","fullname":"鹤壁市","level":"city","filename":"410000/410600","children":[{"code":410602,"name":"鹤山","fullname":"鹤山区","level":"district","filename":"410000/410600/410602"},{"code":410603,"name":"山城","fullname":"山城区","level":"district","filename":"410000/410600/410603"},{"code":410611,"name":"淇滨","fullname":"淇滨区","level":"district","filename":"410000/410600/410611"},{"code":410621,"name":"浚县","fullname":"浚县","level":"district","filename":"410000/410600/410621"},{"code":410622,"name":"淇县","fullname":"淇县","level":"district","filename":"410000/410600/410622"}]},{"code":410700,"name":"新乡","fullname":"新乡市","level":"city","filename":"410000/410700","children":[{"code":410702,"name":"红旗区","fullname":"红旗区","level":"district","filename":"410000/410700/410702"},{"code":410703,"name":"卫滨","fullname":"卫滨区","level":"district","filename":"410000/410700/410703"},{"code":410704,"name":"凤泉","fullname":"凤泉区","level":"district","filename":"410000/410700/410704"},{"code":410711,"name":"牧野","fullname":"牧野区","level":"district","filename":"410000/410700/410711"},{"code":410721,"name":"新乡","fullname":"新乡县","level":"district","filename":"410000/410700/410721"},{"code":410724,"name":"获嘉","fullname":"获嘉县","level":"district","filename":"410000/410700/410724"},{"code":410725,"name":"原阳","fullname":"原阳县","level":"district","filename":"410000/410700/410725"},{"code":410726,"name":"延津","fullname":"延津县","level":"district","filename":"410000/410700/410726"},{"code":410727,"name":"封丘","fullname":"封丘县","level":"district","filename":"410000/410700/410727"},{"code":410781,"name":"卫辉","fullname":"卫辉市","level":"district","filename":"410000/410700/410781"},{"code":410782,"name":"辉县市","fullname":"辉县市","level":"district","filename":"410000/410700/410782"},{"code":410783,"name":"长垣","fullname":"长垣市","level":"district","filename":"410000/410700/410783"}]},{"code":410800,"name":"焦作","fullname":"焦作市","level":"city","filename":"410000/410800","children":[{"code":410802,"name":"解放","fullname":"解放区","level":"district","filename":"410000/410800/410802"},{"code":410803,"name":"中站","fullname":"中站区","level":"district","filename":"410000/410800/410803"},{"code":410804,"name":"马村","fullname":"马村区","level":"district","filename":"410000/410800/410804"},{"code":410811,"name":"山阳","fullname":"山阳区","level":"district","filename":"410000/410800/410811"},{"code":410821,"name":"修武","fullname":"修武县","level":"district","filename":"410000/410800/410821"},{"code":410822,"name":"博爱","fullname":"博爱县","level":"district","filename":"410000/410800/410822"},{"code":410823,"name":"武陟","fullname":"武陟县","level":"district","filename":"410000/410800/410823"},{"code":410825,"name":"温县","fullname":"温县","level":"district","filename":"410000/410800/410825"},{"code":410882,"name":"沁阳","fullname":"沁阳市","level":"district","filename":"410000/410800/410882"},{"code":410883,"name":"孟州","fullname":"孟州市","level":"district","filename":"410000/410800/410883"}]},{"code":410900,"name":"濮阳","fullname":"濮阳市","level":"city","filename":"410000/410900","children":[{"code":410902,"name":"华龙","fullname":"华龙区","level":"district","filename":"410000/410900/410902"},{"code":410922,"name":"清丰","fullname":"清丰县","level":"district","filename":"410000/410900/410922"},{"code":410923,"name":"南乐","fullname":"南乐县","level":"district","filename":"410000/410900/410923"},{"code":410926,"name":"范县","fullname":"范县","level":"district","filename":"410000/410900/410926"},{"code":410927,"name":"台前","fullname":"台前县","level":"district","filename":"410000/410900/410927"},{"code":410928,"name":"濮阳","fullname":"濮阳县","level":"district","filename":"410000/410900/410928"}]},{"code":411000,"name":"许昌","fullname":"许昌市","level":"city","filename":"410000/411000","children":[{"code":411002,"name":"魏都","fullname":"魏都区","level":"district","filename":"410000/411000/411002"},{"code":411003,"name":"建安","fullname":"建安区","level":"district","filename":"410000/411000/411003"},{"code":411024,"name":"鄢陵","fullname":"鄢陵县","level":"district","filename":"410000/411000/411024"},{"code":411025,"name":"襄城","fullname":"襄城县","level":"district","filename":"410000/411000/411025"},{"code":411081,"name":"禹州","fullname":"禹州市","level":"district","filename":"410000/411000/411081"},{"code":411082,"name":"长葛","fullname":"长葛市","level":"district","filename":"410000/411000/411082"}]},{"code":411100,"name":"漯河","fullname":"漯河市","level":"city","filename":"410000/411100","children":[{"code":411102,"name":"源汇","fullname":"源汇区","level":"district","filename":"410000/411100/411102"},{"code":411103,"name":"郾城","fullname":"郾城区","level":"district","filename":"410000/411100/411103"},{"code":411104,"name":"召陵","fullname":"召陵区","level":"district","filename":"410000/411100/411104"},{"code":411121,"name":"舞阳","fullname":"舞阳县","level":"district","filename":"410000/411100/411121"},{"code":411122,"name":"临颍","fullname":"临颍县","level":"district","filename":"410000/411100/411122"}]},{"code":411200,"name":"三门峡","fullname":"三门峡市","level":"city","filename":"410000/411200","children":[{"code":411202,"name":"湖滨","fullname":"湖滨区","level":"district","filename":"410000/411200/411202"},{"code":411203,"name":"陕州","fullname":"陕州区","level":"district","filename":"410000/411200/411203"},{"code":411221,"name":"渑池","fullname":"渑池县","level":"district","filename":"410000/411200/411221"},{"code":411224,"name":"卢氏","fullname":"卢氏县","level":"district","filename":"410000/411200/411224"},{"code":411281,"name":"义马","fullname":"义马市","level":"district","filename":"410000/411200/411281"},{"code":411282,"name":"灵宝","fullname":"灵宝市","level":"district","filename":"410000/411200/411282"}]},{"code":411300,"name":"南阳","fullname":"南阳市","level":"city","filename":"410000/411300","children":[{"code":411302,"name":"宛城","fullname":"宛城区","level":"district","filename":"410000/411300/411302"},{"code":411303,"name":"卧龙","fullname":"卧龙区","level":"district","filename":"410000/411300/411303"},{"code":411321,"name":"南召","fullname":"南召县","level":"district","filename":"410000/411300/411321"},{"code":411322,"name":"方城","fullname":"方城县","level":"district","filename":"410000/411300/411322"},{"code":411323,"name":"西峡","fullname":"西峡县","level":"district","filename":"410000/411300/411323"},{"code":411324,"name":"镇平","fullname":"镇平县","level":"district","filename":"410000/411300/411324"},{"code":411325,"name":"内乡","fullname":"内乡县","level":"district","filename":"410000/411300/411325"},{"code":411326,"name":"淅川","fullname":"淅川县","level":"district","filename":"410000/411300/411326"},{"code":411327,"name":"社旗县","fullname":"社旗县","level":"district","filename":"410000/411300/411327"},{"code":411328,"name":"唐河","fullname":"唐河县","level":"district","filename":"410000/411300/411328"},{"code":411329,"name":"新野","fullname":"新野县","level":"district","filename":"410000/411300/411329"},{"code":411330,"name":"桐柏","fullname":"桐柏县","level":"district","filename":"410000/411300/411330"},{"code":411381,"name":"邓州","fullname":"邓州市","level":"district","filename":"410000/411300/411381"}]},{"code":411400,"name":"商丘","fullname":"商丘市","level":"city","filename":"410000/411400","children":[{"code":411402,"name":"梁园","fullname":"梁园区","level":"district","filename":"410000/411400/411402"},{"code":411403,"name":"睢阳","fullname":"睢阳区","level":"district","filename":"410000/411400/411403"},{"code":411421,"name":"民权","fullname":"民权县","level":"district","filename":"410000/411400/411421"},{"code":411422,"name":"睢县","fullname":"睢县","level":"district","filename":"410000/411400/411422"},{"code":411423,"name":"宁陵","fullname":"宁陵县","level":"district","filename":"410000/411400/411423"},{"code":411424,"name":"柘城","fullname":"柘城县","level":"district","filename":"410000/411400/411424"},{"code":411425,"name":"虞城","fullname":"虞城县","level":"district","filename":"410000/411400/411425"},{"code":411426,"name":"夏邑","fullname":"夏邑县","level":"district","filename":"410000/411400/411426"},{"code":411481,"name":"永城","fullname":"永城市","level":"district","filename":"410000/411400/411481"}]},{"code":411500,"name":"信阳","fullname":"信阳市","level":"city","filename":"410000/411500","children":[{"code":411502,"name":"浉河","fullname":"浉河区","level":"district","filename":"410000/411500/411502"},{"code":411503,"name":"平桥","fullname":"平桥区","level":"district","filename":"410000/411500/411503"},{"code":411521,"name":"罗山","fullname":"罗山县","level":"district","filename":"410000/411500/411521"},{"code":411522,"name":"光山","fullname":"光山县","level":"district","filename":"410000/411500/411522"},{"code":411523,"name":"新县","fullname":"新县","level":"district","filename":"410000/411500/411523"},{"code":411524,"name":"商城","fullname":"商城县","level":"district","filename":"410000/411500/411524"},{"code":411525,"name":"固始","fullname":"固始县","level":"district","filename":"410000/411500/411525"},{"code":411526,"name":"潢川","fullname":"潢川县","level":"district","filename":"410000/411500/411526"},{"code":411527,"name":"淮滨","fullname":"淮滨县","level":"district","filename":"410000/411500/411527"},{"code":411528,"name":"息县","fullname":"息县","level":"district","filename":"410000/411500/411528"}]},{"code":411600,"name":"周口","fullname":"周口市","level":"city","filename":"410000/411600","children":[{"code":411602,"name":"川汇","fullname":"川汇区","level":"district","filename":"410000/411600/411602"},{"code":411603,"name":"淮阳","fullname":"淮阳区","level":"district","filename":"410000/411600/411603"},{"code":411621,"name":"扶沟","fullname":"扶沟县","level":"district","filename":"410000/411600/411621"},{"code":411622,"name":"西华","fullname":"西华县","level":"district","filename":"410000/411600/411622"},{"code":411623,"name":"商水","fullname":"商水县","level":"district","filename":"410000/411600/411623"},{"code":411624,"name":"沈丘","fullname":"沈丘县","level":"district","filename":"410000/411600/411624"},{"code":411625,"name":"郸城","fullname":"郸城县","level":"district","filename":"410000/411600/411625"},{"code":411627,"name":"太康","fullname":"太康县","level":"district","filename":"410000/411600/411627"},{"code":411628,"name":"鹿邑","fullname":"鹿邑县","level":"district","filename":"410000/411600/411628"},{"code":411681,"name":"项城","fullname":"项城市","level":"district","filename":"410000/411600/411681"}]},{"code":411700,"name":"驻马店","fullname":"驻马店市","level":"city","filename":"410000/411700","children":[{"code":411702,"name":"驿城","fullname":"驿城区","level":"district","filename":"410000/411700/411702"},{"code":411721,"name":"西平","fullname":"西平县","level":"district","filename":"410000/411700/411721"},{"code":411722,"name":"上蔡","fullname":"上蔡县","level":"district","filename":"410000/411700/411722"},{"code":411723,"name":"平舆","fullname":"平舆县","level":"district","filename":"410000/411700/411723"},{"code":411724,"name":"正阳","fullname":"正阳县","level":"district","filename":"410000/411700/411724"},{"code":411725,"name":"确山","fullname":"确山县","level":"district","filename":"410000/411700/411725"},{"code":411726,"name":"泌阳","fullname":"泌阳县","level":"district","filename":"410000/411700/411726"},{"code":411727,"name":"汝南","fullname":"汝南县","level":"district","filename":"410000/411700/411727"},{"code":411728,"name":"遂平","fullname":"遂平县","level":"district","filename":"410000/411700/411728"},{"code":411729,"name":"新蔡","fullname":"新蔡县","level":"district","filename":"410000/411700/411729"}]},{"code":419001,"name":"济源","fullname":"济源市","level":"city","filename":"410000/419001"}]},{"code":420000,"name":"湖北","fullname":"湖北省","level":"province","filename":"420000","children":[{"code":420100,"name":"武汉","fullname":"武汉市","level":"city","filename":"420000/420100","children":[{"code":420102,"name":"江岸","fullname":"江岸区","level":"district","filename":"420000/420100/420102"},{"code":420103,"name":"江汉","fullname":"江汉区","level":"district","filename":"420000/420100/420103"},{"code":420104,"name":"硚口","fullname":"硚口区","level":"district","filename":"420000/420100/420104"},{"code":420105,"name":"汉阳","fullname":"汉阳区","level":"district","filename":"420000/420100/420105"},{"code":420106,"name":"武昌","fullname":"武昌区","level":"district","filename":"420000/420100/420106"},{"code":420107,"name":"青山","fullname":"青山区","level":"district","filename":"420000/420100/420107"},{"code":420111,"name":"洪山","fullname":"洪山区","level":"district","filename":"420000/420100/420111"},{"code":420112,"name":"东西湖","fullname":"东西湖区","level":"district","filename":"420000/420100/420112"},{"code":420113,"name":"汉南","fullname":"汉南区","level":"district","filename":"420000/420100/420113"},{"code":420114,"name":"蔡甸","fullname":"蔡甸区","level":"district","filename":"420000/420100/420114"},{"code":420115,"name":"江夏","fullname":"江夏区","level":"district","filename":"420000/420100/420115"},{"code":420116,"name":"黄陂","fullname":"黄陂区","level":"district","filename":"420000/420100/420116"},{"code":420117,"name":"新洲","fullname":"新洲区","level":"district","filename":"420000/420100/420117"}]},{"code":420200,"name":"黄石","fullname":"黄石市","level":"city","filename":"420000/420200","children":[{"code":420202,"name":"黄石港","fullname":"黄石港区","level":"district","filename":"420000/420200/420202"},{"code":420203,"name":"西塞山","fullname":"西塞山区","level":"district","filename":"420000/420200/420203"},{"code":420204,"name":"下陆","fullname":"下陆区","level":"district","filename":"420000/420200/420204"},{"code":420205,"name":"铁山","fullname":"铁山区","level":"district","filename":"420000/420200/420205"},{"code":420222,"name":"阳新","fullname":"阳新县","level":"district","filename":"420000/420200/420222"},{"code":420281,"name":"大冶","fullname":"大冶市","level":"district","filename":"420000/420200/420281"}]},{"code":420300,"name":"十堰","fullname":"十堰市","level":"city","filename":"420000/420300","children":[{"code":420302,"name":"茅箭","fullname":"茅箭区","level":"district","filename":"420000/420300/420302"},{"code":420303,"name":"张湾","fullname":"张湾区","level":"district","filename":"420000/420300/420303"},{"code":420304,"name":"郧阳","fullname":"郧阳区","level":"district","filename":"420000/420300/420304"},{"code":420322,"name":"郧西","fullname":"郧西县","level":"district","filename":"420000/420300/420322"},{"code":420323,"name":"竹山","fullname":"竹山县","level":"district","filename":"420000/420300/420323"},{"code":420324,"name":"竹溪","fullname":"竹溪县","level":"district","filename":"420000/420300/420324"},{"code":420325,"name":"房县","fullname":"房县","level":"district","filename":"420000/420300/420325"},{"code":420381,"name":"丹江口","fullname":"丹江口市","level":"district","filename":"420000/420300/420381"}]},{"code":420500,"name":"宜昌","fullname":"宜昌市","level":"city","filename":"420000/420500","children":[{"code":420502,"name":"西陵","fullname":"西陵区","level":"district","filename":"420000/420500/420502"},{"code":420503,"name":"伍家岗","fullname":"伍家岗区","level":"district","filename":"420000/420500/420503"},{"code":420504,"name":"点军","fullname":"点军区","level":"district","filename":"420000/420500/420504"},{"code":420505,"name":"猇亭","fullname":"猇亭区","level":"district","filename":"420000/420500/420505"},{"code":420506,"name":"夷陵","fullname":"夷陵区","level":"district","filename":"420000/420500/420506"},{"code":420525,"name":"远安","fullname":"远安县","level":"district","filename":"420000/420500/420525"},{"code":420526,"name":"兴山","fullname":"兴山县","level":"district","filename":"420000/420500/420526"},{"code":420527,"name":"秭归","fullname":"秭归县","level":"district","filename":"420000/420500/420527"},{"code":420528,"name":"长阳","fullname":"长阳土家族自治县","level":"district","filename":"420000/420500/420528"},{"code":420529,"name":"五峰","fullname":"五峰土家族自治县","level":"district","filename":"420000/420500/420529"},{"code":420581,"name":"宜都","fullname":"宜都市","level":"district","filename":"420000/420500/420581"},{"code":420582,"name":"当阳","fullname":"当阳市","level":"district","filename":"420000/420500/420582"},{"code":420583,"name":"枝江","fullname":"枝江市","level":"district","filename":"420000/420500/420583"}]},{"code":420600,"name":"襄阳","fullname":"襄阳市","level":"city","filename":"420000/420600","children":[{"code":420602,"name":"襄城","fullname":"襄城区","level":"district","filename":"420000/420600/420602"},{"code":420606,"name":"樊城","fullname":"樊城区","level":"district","filename":"420000/420600/420606"},{"code":420607,"name":"襄州","fullname":"襄州区","level":"district","filename":"420000/420600/420607"},{"code":420624,"name":"南漳","fullname":"南漳县","level":"district","filename":"420000/420600/420624"},{"code":420625,"name":"谷城","fullname":"谷城县","level":"district","filename":"420000/420600/420625"},{"code":420626,"name":"保康","fullname":"保康县","level":"district","filename":"420000/420600/420626"},{"code":420682,"name":"老河口","fullname":"老河口市","level":"district","filename":"420000/420600/420682"},{"code":420683,"name":"枣阳","fullname":"枣阳市","level":"district","filename":"420000/420600/420683"},{"code":420684,"name":"宜城","fullname":"宜城市","level":"district","filename":"420000/420600/420684"}]},{"code":420700,"name":"鄂州","fullname":"鄂州市","level":"city","filename":"420000/420700","children":[{"code":420702,"name":"梁子湖","fullname":"梁子湖区","level":"district","filename":"420000/420700/420702"},{"code":420703,"name":"华容","fullname":"华容区","level":"district","filename":"420000/420700/420703"},{"code":420704,"name":"鄂城","fullname":"鄂城区","level":"district","filename":"420000/420700/420704"}]},{"code":420800,"name":"荆门","fullname":"荆门市","level":"city","filename":"420000/420800","children":[{"code":420802,"name":"东宝","fullname":"东宝区","level":"district","filename":"420000/420800/420802"},{"code":420804,"name":"掇刀","fullname":"掇刀区","level":"district","filename":"420000/420800/420804"},{"code":420822,"name":"沙洋","fullname":"沙洋县","level":"district","filename":"420000/420800/420822"},{"code":420881,"name":"钟祥","fullname":"钟祥市","level":"district","filename":"420000/420800/420881"},{"code":420882,"name":"京山","fullname":"京山市","level":"district","filename":"420000/420800/420882"}]},{"code":420900,"name":"孝感","fullname":"孝感市","level":"city","filename":"420000/420900","children":[{"code":420902,"name":"孝南","fullname":"孝南区","level":"district","filename":"420000/420900/420902"},{"code":420921,"name":"孝昌","fullname":"孝昌县","level":"district","filename":"420000/420900/420921"},{"code":420922,"name":"大悟","fullname":"大悟县","level":"district","filename":"420000/420900/420922"},{"code":420923,"name":"云梦","fullname":"云梦县","level":"district","filename":"420000/420900/420923"},{"code":420981,"name":"应城","fullname":"应城市","level":"district","filename":"420000/420900/420981"},{"code":420982,"name":"安陆","fullname":"安陆市","level":"district","filename":"420000/420900/420982"},{"code":420984,"name":"汉川","fullname":"汉川市","level":"district","filename":"420000/420900/420984"}]},{"code":421000,"name":"荆州","fullname":"荆州市","level":"city","filename":"420000/421000","children":[{"code":421002,"name":"沙市区","fullname":"沙市区","level":"district","filename":"420000/421000/421002"},{"code":421003,"name":"荆州","fullname":"荆州区","level":"district","filename":"420000/421000/421003"},{"code":421022,"name":"公安","fullname":"公安县","level":"district","filename":"420000/421000/421022"},{"code":421023,"name":"监利","fullname":"监利市","level":"district","filename":"420000/421000/421023"},{"code":421024,"name":"江陵","fullname":"江陵县","level":"district","filename":"420000/421000/421024"},{"code":421081,"name":"石首","fullname":"石首市","level":"district","filename":"420000/421000/421081"},{"code":421083,"name":"洪湖","fullname":"洪湖市","level":"district","filename":"420000/421000/421083"},{"code":421087,"name":"松滋","fullname":"松滋市","level":"district","filename":"420000/421000/421087"}]},{"code":421100,"name":"黄冈","fullname":"黄冈市","level":"city","filename":"420000/421100","children":[{"code":421102,"name":"黄州","fullname":"黄州区","level":"district","filename":"420000/421100/421102"},{"code":421121,"name":"团风","fullname":"团风县","level":"district","filename":"420000/421100/421121"},{"code":421122,"name":"红安","fullname":"红安县","level":"district","filename":"420000/421100/421122"},{"code":421123,"name":"罗田","fullname":"罗田县","level":"district","filename":"420000/421100/421123"},{"code":421124,"name":"英山","fullname":"英山县","level":"district","filename":"420000/421100/421124"},{"code":421125,"name":"浠水","fullname":"浠水县","level":"district","filename":"420000/421100/421125"},{"code":421126,"name":"蕲春","fullname":"蕲春县","level":"district","filename":"420000/421100/421126"},{"code":421127,"name":"黄梅","fullname":"黄梅县","level":"district","filename":"420000/421100/421127"},{"code":421181,"name":"麻城","fullname":"麻城市","level":"district","filename":"420000/421100/421181"},{"code":421182,"name":"武穴","fullname":"武穴市","level":"district","filename":"420000/421100/421182"}]},{"code":421200,"name":"咸宁","fullname":"咸宁市","level":"city","filename":"420000/421200","children":[{"code":421202,"name":"咸安","fullname":"咸安区","level":"district","filename":"420000/421200/421202"},{"code":421221,"name":"嘉鱼","fullname":"嘉鱼县","level":"district","filename":"420000/421200/421221"},{"code":421222,"name":"通城","fullname":"通城县","level":"district","filename":"420000/421200/421222"},{"code":421223,"name":"崇阳","fullname":"崇阳县","level":"district","filename":"420000/421200/421223"},{"code":421224,"name":"通山","fullname":"通山县","level":"district","filename":"420000/421200/421224"},{"code":421281,"name":"赤壁","fullname":"赤壁市","level":"district","filename":"420000/421200/421281"}]},{"code":421300,"name":"随州","fullname":"随州市","level":"city","filename":"420000/421300","children":[{"code":421303,"name":"曾都","fullname":"曾都区","level":"district","filename":"420000/421300/421303"},{"code":421321,"name":"随县","fullname":"随县","level":"district","filename":"420000/421300/421321"},{"code":421381,"name":"广水","fullname":"广水市","level":"district","filename":"420000/421300/421381"}]},{"code":422800,"name":"恩施","fullname":"恩施土家族苗族自治州","level":"city","filename":"420000/422800","children":[{"code":422801,"name":"恩施","fullname":"恩施市","level":"district","filename":"420000/422800/422801"},{"code":422802,"name":"利川","fullname":"利川市","level":"district","filename":"420000/422800/422802"},{"code":422822,"name":"建始","fullname":"建始县","level":"district","filename":"420000/422800/422822"},{"code":422823,"name":"巴东","fullname":"巴东县","level":"district","filename":"420000/422800/422823"},{"code":422825,"name":"宣恩","fullname":"宣恩县","level":"district","filename":"420000/422800/422825"},{"code":422826,"name":"咸丰","fullname":"咸丰县","level":"district","filename":"420000/422800/422826"},{"code":422827,"name":"来凤","fullname":"来凤县","level":"district","filename":"420000/422800/422827"},{"code":422828,"name":"鹤峰","fullname":"鹤峰县","level":"district","filename":"420000/422800/422828"}]},{"code":429004,"name":"仙桃","fullname":"仙桃市","level":"city","filename":"420000/429004"},{"code":429005,"name":"潜江","fullname":"潜江市","level":"city","filename":"420000/429005"},{"code":429006,"name":"天门","fullname":"天门市","level":"city","filename":"420000/429006"},{"code":429021,"name":"神农架","fullname":"神农架林区","level":"city","filename":"420000/429021"}]},{"code":430000,"name":"湖南","fullname":"湖南省","level":"province","filename":"430000","children":[{"code":430100,"name":"长沙","fullname":"长沙市","level":"city","filename":"430000/430100","children":[{"code":430102,"name":"芙蓉","fullname":"芙蓉区","level":"district","filename":"430000/430100/430102"},{"code":430103,"name":"天心","fullname":"天心区","level":"district","filename":"430000/430100/430103"},{"code":430104,"name":"岳麓","fullname":"岳麓区","level":"district","filename":"430000/430100/430104"},{"code":430105,"name":"开福","fullname":"开福区","level":"district","filename":"430000/430100/430105"},{"code":430111,"name":"雨花","fullname":"雨花区","level":"district","filename":"430000/430100/430111"},{"code":430112,"name":"望城","fullname":"望城区","level":"district","filename":"430000/430100/430112"},{"code":430121,"name":"长沙","fullname":"长沙县","level":"district","filename":"430000/430100/430121"},{"code":430181,"name":"浏阳","fullname":"浏阳市","level":"district","filename":"430000/430100/430181"},{"code":430182,"name":"宁乡","fullname":"宁乡市","level":"district","filename":"430000/430100/430182"}]},{"code":430200,"name":"株洲","fullname":"株洲市","level":"city","filename":"430000/430200","children":[{"code":430202,"name":"荷塘","fullname":"荷塘区","level":"district","filename":"430000/430200/430202"},{"code":430203,"name":"芦淞","fullname":"芦淞区","level":"district","filename":"430000/430200/430203"},{"code":430204,"name":"石峰","fullname":"石峰区","level":"district","filename":"430000/430200/430204"},{"code":430211,"name":"天元","fullname":"天元区","level":"district","filename":"430000/430200/430211"},{"code":430212,"name":"渌口","fullname":"渌口区","level":"district","filename":"430000/430200/430212"},{"code":430223,"name":"攸县","fullname":"攸县","level":"district","filename":"430000/430200/430223"},{"code":430224,"name":"茶陵","fullname":"茶陵县","level":"district","filename":"430000/430200/430224"},{"code":430225,"name":"炎陵","fullname":"炎陵县","level":"district","filename":"430000/430200/430225"},{"code":430281,"name":"醴陵","fullname":"醴陵市","level":"district","filename":"430000/430200/430281"}]},{"code":430300,"name":"湘潭","fullname":"湘潭市","level":"city","filename":"430000/430300","children":[{"code":430302,"name":"雨湖","fullname":"雨湖区","level":"district","filename":"430000/430300/430302"},{"code":430304,"name":"岳塘","fullname":"岳塘区","level":"district","filename":"430000/430300/430304"},{"code":430321,"name":"湘潭","fullname":"湘潭县","level":"district","filename":"430000/430300/430321"},{"code":430381,"name":"湘乡","fullname":"湘乡市","level":"district","filename":"430000/430300/430381"},{"code":430382,"name":"韶山","fullname":"韶山市","level":"district","filename":"430000/430300/430382"}]},{"code":430400,"name":"衡阳","fullname":"衡阳市","level":"city","filename":"430000/430400","children":[{"code":430405,"name":"珠晖","fullname":"珠晖区","level":"district","filename":"430000/430400/430405"},{"code":430406,"name":"雁峰","fullname":"雁峰区","level":"district","filename":"430000/430400/430406"},{"code":430407,"name":"石鼓","fullname":"石鼓区","level":"district","filename":"430000/430400/430407"},{"code":430408,"name":"蒸湘","fullname":"蒸湘区","level":"district","filename":"430000/430400/430408"},{"code":430412,"name":"南岳","fullname":"南岳区","level":"district","filename":"430000/430400/430412"},{"code":430421,"name":"衡阳","fullname":"衡阳县","level":"district","filename":"430000/430400/430421"},{"code":430422,"name":"衡南","fullname":"衡南县","level":"district","filename":"430000/430400/430422"},{"code":430423,"name":"衡山","fullname":"衡山县","level":"district","filename":"430000/430400/430423"},{"code":430424,"name":"衡东","fullname":"衡东县","level":"district","filename":"430000/430400/430424"},{"code":430426,"name":"祁东","fullname":"祁东县","level":"district","filename":"430000/430400/430426"},{"code":430481,"name":"耒阳","fullname":"耒阳市","level":"district","filename":"430000/430400/430481"},{"code":430482,"name":"常宁","fullname":"常宁市","level":"district","filename":"430000/430400/430482"}]},{"code":430500,"name":"邵阳","fullname":"邵阳市","level":"city","filename":"430000/430500","children":[{"code":430502,"name":"双清","fullname":"双清区","level":"district","filename":"430000/430500/430502"},{"code":430503,"name":"大祥","fullname":"大祥区","level":"district","filename":"430000/430500/430503"},{"code":430511,"name":"北塔","fullname":"北塔区","level":"district","filename":"430000/430500/430511"},{"code":430522,"name":"新邵","fullname":"新邵县","level":"district","filename":"430000/430500/430522"},{"code":430523,"name":"邵阳","fullname":"邵阳县","level":"district","filename":"430000/430500/430523"},{"code":430524,"name":"隆回","fullname":"隆回县","level":"district","filename":"430000/430500/430524"},{"code":430525,"name":"洞口","fullname":"洞口县","level":"district","filename":"430000/430500/430525"},{"code":430527,"name":"绥宁","fullname":"绥宁县","level":"district","filename":"430000/430500/430527"},{"code":430528,"name":"新宁","fullname":"新宁县","level":"district","filename":"430000/430500/430528"},{"code":430529,"name":"城步","fullname":"城步苗族自治县","level":"district","filename":"430000/430500/430529"},{"code":430581,"name":"武冈","fullname":"武冈市","level":"district","filename":"430000/430500/430581"},{"code":430582,"name":"邵东","fullname":"邵东市","level":"district","filename":"430000/430500/430582"}]},{"code":430600,"name":"岳阳","fullname":"岳阳市","level":"city","filename":"430000/430600","children":[{"code":430602,"name":"岳阳楼","fullname":"岳阳楼区","level":"district","filename":"430000/430600/430602"},{"code":430603,"name":"云溪","fullname":"云溪区","level":"district","filename":"430000/430600/430603"},{"code":430611,"name":"君山","fullname":"君山区","level":"district","filename":"430000/430600/430611"},{"code":430621,"name":"岳阳","fullname":"岳阳县","level":"district","filename":"430000/430600/430621"},{"code":430623,"name":"华容","fullname":"华容县","level":"district","filename":"430000/430600/430623"},{"code":430624,"name":"湘阴","fullname":"湘阴县","level":"district","filename":"430000/430600/430624"},{"code":430626,"name":"平江","fullname":"平江县","level":"district","filename":"430000/430600/430626"},{"code":430681,"name":"汨罗","fullname":"汨罗市","level":"district","filename":"430000/430600/430681"},{"code":430682,"name":"临湘","fullname":"临湘市","level":"district","filename":"430000/430600/430682"}]},{"code":430700,"name":"常德","fullname":"常德市","level":"city","filename":"430000/430700","children":[{"code":430702,"name":"武陵","fullname":"武陵区","level":"district","filename":"430000/430700/430702"},{"code":430703,"name":"鼎城","fullname":"鼎城区","level":"district","filename":"430000/430700/430703"},{"code":430721,"name":"安乡","fullname":"安乡县","level":"district","filename":"430000/430700/430721"},{"code":430722,"name":"汉寿","fullname":"汉寿县","level":"district","filename":"430000/430700/430722"},{"code":430723,"name":"澧县","fullname":"澧县","level":"district","filename":"430000/430700/430723"},{"code":430724,"name":"临澧","fullname":"临澧县","level":"district","filename":"430000/430700/430724"},{"code":430725,"name":"桃源","fullname":"桃源县","level":"district","filename":"430000/430700/430725"},{"code":430726,"name":"石门","fullname":"石门县","level":"district","filename":"430000/430700/430726"},{"code":430781,"name":"津市市","fullname":"津市市","level":"district","filename":"430000/430700/430781"}]},{"code":430800,"name":"张家界","fullname":"张家界市","level":"city","filename":"430000/430800","children":[{"code":430802,"name":"永定","fullname":"永定区","level":"district","filename":"430000/430800/430802"},{"code":430811,"name":"武陵源","fullname":"武陵源区","level":"district","filename":"430000/430800/430811"},{"code":430821,"name":"慈利","fullname":"慈利县","level":"district","filename":"430000/430800/430821"},{"code":430822,"name":"桑植","fullname":"桑植县","level":"district","filename":"430000/430800/430822"}]},{"code":430900,"name":"益阳","fullname":"益阳市","level":"city","filename":"430000/430900","children":[{"code":430902,"name":"资阳","fullname":"资阳区","level":"district","filename":"430000/430900/430902"},{"code":430903,"name":"赫山","fullname":"赫山区","level":"district","filename":"430000/430900/430903"},{"code":430921,"name":"南县","fullname":"南县","level":"district","filename":"430000/430900/430921"},{"code":430922,"name":"桃江","fullname":"桃江县","level":"district","filename":"430000/430900/430922"},{"code":430923,"name":"安化","fullname":"安化县","level":"district","filename":"430000/430900/430923"},{"code":430981,"name":"沅江","fullname":"沅江市","level":"district","filename":"430000/430900/430981"}]},{"code":431000,"name":"郴州","fullname":"郴州市","level":"city","filename":"430000/431000","children":[{"code":431002,"name":"北湖","fullname":"北湖区","level":"district","filename":"430000/431000/431002"},{"code":431003,"name":"苏仙","fullname":"苏仙区","level":"district","filename":"430000/431000/431003"},{"code":431021,"name":"桂阳","fullname":"桂阳县","level":"district","filename":"430000/431000/431021"},{"code":431022,"name":"宜章","fullname":"宜章县","level":"district","filename":"430000/431000/431022"},{"code":431023,"name":"永兴","fullname":"永兴县","level":"district","filename":"430000/431000/431023"},{"code":431024,"name":"嘉禾","fullname":"嘉禾县","level":"district","filename":"430000/431000/431024"},{"code":431025,"name":"临武","fullname":"临武县","level":"district","filename":"430000/431000/431025"},{"code":431026,"name":"汝城","fullname":"汝城县","level":"district","filename":"430000/431000/431026"},{"code":431027,"name":"桂东","fullname":"桂东县","level":"district","filename":"430000/431000/431027"},{"code":431028,"name":"安仁","fullname":"安仁县","level":"district","filename":"430000/431000/431028"},{"code":431081,"name":"资兴","fullname":"资兴市","level":"district","filename":"430000/431000/431081"}]},{"code":431100,"name":"永州","fullname":"永州市","level":"city","filename":"430000/431100","children":[{"code":431102,"name":"零陵","fullname":"零陵区","level":"district","filename":"430000/431100/431102"},{"code":431103,"name":"冷水滩","fullname":"冷水滩区","level":"district","filename":"430000/431100/431103"},{"code":431121,"name":"祁阳","fullname":"祁阳市","level":"district","filename":"430000/431100/431121"},{"code":431122,"name":"东安","fullname":"东安县","level":"district","filename":"430000/431100/431122"},{"code":431123,"name":"双牌","fullname":"双牌县","level":"district","filename":"430000/431100/431123"},{"code":431124,"name":"道县","fullname":"道县","level":"district","filename":"430000/431100/431124"},{"code":431125,"name":"江永","fullname":"江永县","level":"district","filename":"430000/431100/431125"},{"code":431126,"name":"宁远","fullname":"宁远县","level":"district","filename":"430000/431100/431126"},{"code":431127,"name":"蓝山","fullname":"蓝山县","level":"district","filename":"430000/431100/431127"},{"code":431128,"name":"新田","fullname":"新田县","level":"district","filename":"430000/431100/431128"},{"code":431129,"name":"江华","fullname":"江华瑶族自治县","level":"district","filename":"430000/431100/431129"}]},{"code":431200,"name":"怀化","fullname":"怀化市","level":"city","filename":"430000/431200","children":[{"code":431202,"name":"鹤城","fullname":"鹤城区","level":"district","filename":"430000/431200/431202"},{"code":431221,"name":"中方","fullname":"中方县","level":"district","filename":"430000/431200/431221"},{"code":431222,"name":"沅陵","fullname":"沅陵县","level":"district","filename":"430000/431200/431222"},{"code":431223,"name":"辰溪","fullname":"辰溪县","level":"district","filename":"430000/431200/431223"},{"code":431224,"name":"溆浦","fullname":"溆浦县","level":"district","filename":"430000/431200/431224"},{"code":431225,"name":"会同","fullname":"会同县","level":"district","filename":"430000/431200/431225"},{"code":431226,"name":"麻阳","fullname":"麻阳苗族自治县","level":"district","filename":"430000/431200/431226"},{"code":431227,"name":"新晃","fullname":"新晃侗族自治县","level":"district","filename":"430000/431200/431227"},{"code":431228,"name":"芷江","fullname":"芷江侗族自治县","level":"district","filename":"430000/431200/431228"},{"code":431229,"name":"靖州","fullname":"靖州苗族侗族自治县","level":"district","filename":"430000/431200/431229"},{"code":431230,"name":"通道","fullname":"通道侗族自治县","level":"district","filename":"430000/431200/431230"},{"code":431281,"name":"洪江","fullname":"洪江市","level":"district","filename":"430000/431200/431281"}]},{"code":431300,"name":"娄底","fullname":"娄底市","level":"city","filename":"430000/431300","children":[{"code":431302,"name":"娄星","fullname":"娄星区","level":"district","filename":"430000/431300/431302"},{"code":431321,"name":"双峰","fullname":"双峰县","level":"district","filename":"430000/431300/431321"},{"code":431322,"name":"新化","fullname":"新化县","level":"district","filename":"430000/431300/431322"},{"code":431381,"name":"冷水江","fullname":"冷水江市","level":"district","filename":"430000/431300/431381"},{"code":431382,"name":"涟源","fullname":"涟源市","level":"district","filename":"430000/431300/431382"}]},{"code":433100,"name":"湘西","fullname":"湘西土家族苗族自治州","level":"city","filename":"430000/433100","children":[{"code":433101,"name":"吉首","fullname":"吉首市","level":"district","filename":"430000/433100/433101"},{"code":433122,"name":"泸溪","fullname":"泸溪县","level":"district","filename":"430000/433100/433122"},{"code":433123,"name":"凤凰","fullname":"凤凰县","level":"district","filename":"430000/433100/433123"},{"code":433124,"name":"花垣","fullname":"花垣县","level":"district","filename":"430000/433100/433124"},{"code":433125,"name":"保靖","fullname":"保靖县","level":"district","filename":"430000/433100/433125"},{"code":433126,"name":"古丈","fullname":"古丈县","level":"district","filename":"430000/433100/433126"},{"code":433127,"name":"永顺","fullname":"永顺县","level":"district","filename":"430000/433100/433127"},{"code":433130,"name":"龙山","fullname":"龙山县","level":"district","filename":"430000/433100/433130"}]}]},{"code":440000,"name":"广东","fullname":"广东省","level":"province","filename":"440000","children":[{"code":440100,"name":"广州","fullname":"广州市","level":"city","filename":"440000/440100","children":[{"code":440103,"name":"荔湾","fullname":"荔湾区","level":"district","filename":"440000/440100/440103"},{"code":440104,"name":"越秀","fullname":"越秀区","level":"district","filename":"440000/440100/440104"},{"code":440105,"name":"海珠","fullname":"海珠区","level":"district","filename":"440000/440100/440105"},{"code":440106,"name":"天河","fullname":"天河区","level":"district","filename":"440000/440100/440106"},{"code":440111,"name":"白云","fullname":"白云区","level":"district","filename":"440000/440100/440111"},{"code":440112,"name":"黄埔","fullname":"黄埔区","level":"district","filename":"440000/440100/440112"},{"code":440113,"name":"番禺","fullname":"番禺区","level":"district","filename":"440000/440100/440113"},{"code":440114,"name":"花都","fullname":"花都区","level":"district","filename":"440000/440100/440114"},{"code":440115,"name":"南沙","fullname":"南沙区","level":"district","filename":"440000/440100/440115"},{"code":440117,"name":"从化","fullname":"从化区","level":"district","filename":"440000/440100/440117"},{"code":440118,"name":"增城","fullname":"增城区","level":"district","filename":"440000/440100/440118"}]},{"code":440200,"name":"韶关","fullname":"韶关市","level":"city","filename":"440000/440200","children":[{"code":440203,"name":"武江","fullname":"武江区","level":"district","filename":"440000/440200/440203"},{"code":440204,"name":"浈江","fullname":"浈江区","level":"district","filename":"440000/440200/440204"},{"code":440205,"name":"曲江","fullname":"曲江区","level":"district","filename":"440000/440200/440205"},{"code":440222,"name":"始兴","fullname":"始兴县","level":"district","filename":"440000/440200/440222"},{"code":440224,"name":"仁化","fullname":"仁化县","level":"district","filename":"440000/440200/440224"},{"code":440229,"name":"翁源","fullname":"翁源县","level":"district","filename":"440000/440200/440229"},{"code":440232,"name":"乳源","fullname":"乳源瑶族自治县","level":"district","filename":"440000/440200/440232"},{"code":440233,"name":"新丰","fullname":"新丰县","level":"district","filename":"440000/440200/440233"},{"code":440281,"name":"乐昌","fullname":"乐昌市","level":"district","filename":"440000/440200/440281"},{"code":440282,"name":"南雄","fullname":"南雄市","level":"district","filename":"440000/440200/440282"}]},{"code":440300,"name":"深圳","fullname":"深圳市","level":"city","filename":"440000/440300","children":[{"code":440303,"name":"罗湖","fullname":"罗湖区","level":"district","filename":"440000/440300/440303"},{"code":440304,"name":"福田","fullname":"福田区","level":"district","filename":"440000/440300/440304"},{"code":440305,"name":"南山","fullname":"南山区","level":"district","filename":"440000/440300/440305"},{"code":440306,"name":"宝安","fullname":"宝安区","level":"district","filename":"440000/440300/440306"},{"code":440307,"name":"龙岗","fullname":"龙岗区","level":"district","filename":"440000/440300/440307"},{"code":440308,"name":"盐田","fullname":"盐田区","level":"district","filename":"440000/440300/440308"},{"code":440309,"name":"龙华","fullname":"龙华区","level":"district","filename":"440000/440300/440309"},{"code":440310,"name":"坪山","fullname":"坪山区","level":"district","filename":"440000/440300/440310"},{"code":440311,"name":"光明","fullname":"光明区","level":"district","filename":"440000/440300/440311"}]},{"code":440400,"name":"珠海","fullname":"珠海市","level":"city","filename":"440000/440400","children":[{"code":440402,"name":"香洲","fullname":"香洲区","level":"district","filename":"440000/440400/440402"},{"code":440403,"name":"斗门","fullname":"斗门区","level":"district","filename":"440000/440400/440403"},{"code":440404,"name":"金湾","fullname":"金湾区","level":"district","filename":"440000/440400/440404"}]},{"code":440500,"name":"汕头","fullname":"汕头市","level":"city","filename":"440000/440500","children":[{"code":440507,"name":"龙湖","fullname":"龙湖区","level":"district","filename":"440000/440500/440507"},{"code":440511,"name":"金平","fullname":"金平区","level":"district","filename":"440000/440500/440511"},{"code":440512,"name":"濠江","fullname":"濠江区","level":"district","filename":"440000/440500/440512"},{"code":440513,"name":"潮阳","fullname":"潮阳区","level":"district","filename":"440000/440500/440513"},{"code":440514,"name":"潮南","fullname":"潮南区","level":"district","filename":"440000/440500/440514"},{"code":440515,"name":"澄海","fullname":"澄海区","level":"district","filename":"440000/440500/440515"},{"code":440523,"name":"南澳","fullname":"南澳县","level":"district","filename":"440000/440500/440523"}]},{"code":440600,"name":"佛山","fullname":"佛山市","level":"city","filename":"440000/440600","children":[{"code":440604,"name":"禅城","fullname":"禅城区","level":"district","filename":"440000/440600/440604"},{"code":440605,"name":"南海","fullname":"南海区","level":"district","filename":"440000/440600/440605"},{"code":440606,"name":"顺德","fullname":"顺德区","level":"district","filename":"440000/440600/440606"},{"code":440607,"name":"三水","fullname":"三水区","level":"district","filename":"440000/440600/440607"},{"code":440608,"name":"高明","fullname":"高明区","level":"district","filename":"440000/440600/440608"}]},{"code":440700,"name":"江门","fullname":"江门市","level":"city","filename":"440000/440700","children":[{"code":440703,"name":"蓬江","fullname":"蓬江区","level":"district","filename":"440000/440700/440703"},{"code":440704,"name":"江海","fullname":"江海区","level":"district","filename":"440000/440700/440704"},{"code":440705,"name":"新会","fullname":"新会区","level":"district","filename":"440000/440700/440705"},{"code":440781,"name":"台山","fullname":"台山市","level":"district","filename":"440000/440700/440781"},{"code":440783,"name":"开平","fullname":"开平市","level":"district","filename":"440000/440700/440783"},{"code":440784,"name":"鹤山","fullname":"鹤山市","level":"district","filename":"440000/440700/440784"},{"code":440785,"name":"恩平","fullname":"恩平市","level":"district","filename":"440000/440700/440785"}]},{"code":440800,"name":"湛江","fullname":"湛江市","level":"city","filename":"440000/440800","children":[{"code":440802,"name":"赤坎","fullname":"赤坎区","level":"district","filename":"440000/440800/440802"},{"code":440803,"name":"霞山","fullname":"霞山区","level":"district","filename":"440000/440800/440803"},{"code":440804,"name":"坡头","fullname":"坡头区","level":"district","filename":"440000/440800/440804"},{"code":440811,"name":"麻章","fullname":"麻章区","level":"district","filename":"440000/440800/440811"},{"code":440823,"name":"遂溪","fullname":"遂溪县","level":"district","filename":"440000/440800/440823"},{"code":440825,"name":"徐闻","fullname":"徐闻县","level":"district","filename":"440000/440800/440825"},{"code":440881,"name":"廉江","fullname":"廉江市","level":"district","filename":"440000/440800/440881"},{"code":440882,"name":"雷州","fullname":"雷州市","level":"district","filename":"440000/440800/440882"},{"code":440883,"name":"吴川","fullname":"吴川市","level":"district","filename":"440000/440800/440883"}]},{"code":440900,"name":"茂名","fullname":"茂名市","level":"city","filename":"440000/440900","children":[{"code":440902,"name":"茂南","fullname":"茂南区","level":"district","filename":"440000/440900/440902"},{"code":440904,"name":"电白","fullname":"电白区","level":"district","filename":"440000/440900/440904"},{"code":440981,"name":"高州","fullname":"高州市","level":"district","filename":"440000/440900/440981"},{"code":440982,"name":"化州","fullname":"化州市","level":"district","filename":"440000/440900/440982"},{"code":440983,"name":"信宜","fullname":"信宜市","level":"district","filename":"440000/440900/440983"}]},{"code":441200,"name":"肇庆","fullname":"肇庆市","level":"city","filename":"440000/441200","children":[{"code":441202,"name":"端州","fullname":"端州区","level":"district","filename":"440000/441200/441202"},{"code":441203,"name":"鼎湖","fullname":"鼎湖区","level":"district","filename":"440000/441200/441203"},{"code":441204,"name":"高要","fullname":"高要区","level":"district","filename":"440000/441200/441204"},{"code":441223,"name":"广宁","fullname":"广宁县","level":"district","filename":"440000/441200/441223"},{"code":441224,"name":"怀集","fullname":"怀集县","level":"district","filename":"440000/441200/441224"},{"code":441225,"name":"封开","fullname":"封开县","level":"district","filename":"440000/441200/441225"},{"code":441226,"name":"德庆","fullname":"德庆县","level":"district","filename":"440000/441200/441226"},{"code":441284,"name":"四会","fullname":"四会市","level":"district","filename":"440000/441200/441284"}]},{"code":441300,"name":"惠州","fullname":"惠州市","level":"city","filename":"440000/441300","children":[{"code":441302,"name":"惠城","fullname":"惠城区","level":"district","filename":"440000/441300/441302"},{"code":441303,"name":"惠阳","fullname":"惠阳区","level":"district","filename":"440000/441300/441303"},{"code":441322,"name":"博罗","fullname":"博罗县","level":"district","filename":"440000/441300/441322"},{"code":441323,"name":"惠东","fullname":"惠东县","level":"district","filename":"440000/441300/441323"},{"code":441324,"name":"龙门","fullname":"龙门县","level":"district","filename":"440000/441300/441324"}]},{"code":441400,"name":"梅州","fullname":"梅州市","level":"city","filename":"440000/441400","children":[{"code":441402,"name":"梅江","fullname":"梅江区","level":"district","filename":"440000/441400/441402"},{"code":441403,"name":"梅县区","fullname":"梅县区","level":"district","filename":"440000/441400/441403"},{"code":441422,"name":"大埔","fullname":"大埔县","level":"district","filename":"440000/441400/441422"},{"code":441423,"name":"丰顺","fullname":"丰顺县","level":"district","filename":"440000/441400/441423"},{"code":441424,"name":"五华","fullname":"五华县","level":"district","filename":"440000/441400/441424"},{"code":441426,"name":"平远","fullname":"平远县","level":"district","filename":"440000/441400/441426"},{"code":441427,"name":"蕉岭","fullname":"蕉岭县","level":"district","filename":"440000/441400/441427"},{"code":441481,"name":"兴宁","fullname":"兴宁市","level":"district","filename":"440000/441400/441481"}]},{"code":441500,"name":"汕尾","fullname":"汕尾市","level":"city","filename":"440000/441500","children":[{"code":441502,"name":"城区","fullname":"城区","level":"district","filename":"440000/441500/441502"},{"code":441521,"name":"海丰","fullname":"海丰县","level":"district","filename":"440000/441500/441521"},{"code":441523,"name":"陆河","fullname":"陆河县","level":"district","filename":"440000/441500/441523"},{"code":441581,"name":"陆丰","fullname":"陆丰市","level":"district","filename":"440000/441500/441581"}]},{"code":441600,"name":"河源","fullname":"河源市","level":"city","filename":"440000/441600","children":[{"code":441602,"name":"源城","fullname":"源城区","level":"district","filename":"440000/441600/441602"},{"code":441621,"name":"紫金","fullname":"紫金县","level":"district","filename":"440000/441600/441621"},{"code":441622,"name":"龙川","fullname":"龙川县","level":"district","filename":"440000/441600/441622"},{"code":441623,"name":"连平","fullname":"连平县","level":"district","filename":"440000/441600/441623"},{"code":441624,"name":"和平","fullname":"和平县","level":"district","filename":"440000/441600/441624"},{"code":441625,"name":"东源","fullname":"东源县","level":"district","filename":"440000/441600/441625"}]},{"code":441700,"name":"阳江","fullname":"阳江市","level":"city","filename":"440000/441700","children":[{"code":441702,"name":"江城","fullname":"江城区","level":"district","filename":"440000/441700/441702"},{"code":441704,"name":"阳东","fullname":"阳东区","level":"district","filename":"440000/441700/441704"},{"code":441721,"name":"阳西","fullname":"阳西县","level":"district","filename":"440000/441700/441721"},{"code":441781,"name":"阳春","fullname":"阳春市","level":"district","filename":"440000/441700/441781"}]},{"code":441800,"name":"清远","fullname":"清远市","level":"city","filename":"440000/441800","children":[{"code":441802,"name":"清城","fullname":"清城区","level":"district","filename":"440000/441800/441802"},{"code":441803,"name":"清新区","fullname":"清新区","level":"district","filename":"440000/441800/441803"},{"code":441821,"name":"佛冈","fullname":"佛冈县","level":"district","filename":"440000/441800/441821"},{"code":441823,"name":"阳山","fullname":"阳山县","level":"district","filename":"440000/441800/441823"},{"code":441825,"name":"连山","fullname":"连山壮族瑶族自治县","level":"district","filename":"440000/441800/441825"},{"code":441826,"name":"连南","fullname":"连南瑶族自治县","level":"district","filename":"440000/441800/441826"},{"code":441881,"name":"英德","fullname":"英德市","level":"district","filename":"440000/441800/441881"},{"code":441882,"name":"连州","fullname":"连州市","level":"district","filename":"440000/441800/441882"}]},{"code":441900,"name":"东莞","fullname":"东莞市","level":"city","filename":"440000/441900"},{"code":442000,"name":"中山","fullname":"中山市","level":"city","filename":"440000/442000"},{"code":445100,"name":"潮州","fullname":"潮州市","level":"city","filename":"440000/445100","children":[{"code":445102,"name":"湘桥","fullname":"湘桥区","level":"district","filename":"440000/445100/445102"},{"code":445103,"name":"潮安","fullname":"潮安区","level":"district","filename":"440000/445100/445103"},{"code":445122,"name":"饶平","fullname":"饶平县","level":"district","filename":"440000/445100/445122"}]},{"code":445200,"name":"揭阳","fullname":"揭阳市","level":"city","filename":"440000/445200","children":[{"code":445202,"name":"榕城","fullname":"榕城区","level":"district","filename":"440000/445200/445202"},{"code":445203,"name":"揭东","fullname":"揭东区","level":"district","filename":"440000/445200/445203"},{"code":445222,"name":"揭西","fullname":"揭西县","level":"district","filename":"440000/445200/445222"},{"code":445224,"name":"惠来","fullname":"惠来县","level":"district","filename":"440000/445200/445224"},{"code":445281,"name":"普宁","fullname":"普宁市","level":"district","filename":"440000/445200/445281"}]},{"code":445300,"name":"云浮","fullname":"云浮市","level":"city","filename":"440000/445300","children":[{"code":445302,"name":"云城","fullname":"云城区","level":"district","filename":"440000/445300/445302"},{"code":445303,"name":"云安","fullname":"云安区","level":"district","filename":"440000/445300/445303"},{"code":445321,"name":"新兴","fullname":"新兴县","level":"district","filename":"440000/445300/445321"},{"code":445322,"name":"郁南","fullname":"郁南县","level":"district","filename":"440000/445300/445322"},{"code":445381,"name":"罗定","fullname":"罗定市","level":"district","filename":"440000/445300/445381"}]}]},{"code":450000,"name":"广西","fullname":"广西壮族自治区","level":"province","filename":"450000","children":[{"code":450100,"name":"南宁","fullname":"南宁市","level":"city","filename":"450000/450100","children":[{"code":450102,"name":"兴宁","fullname":"兴宁区","level":"district","filename":"450000/450100/450102"},{"code":450103,"name":"青秀","fullname":"青秀区","level":"district","filename":"450000/450100/450103"},{"code":450105,"name":"江南","fullname":"江南区","level":"district","filename":"450000/450100/450105"},{"code":450107,"name":"西乡塘","fullname":"西乡塘区","level":"district","filename":"450000/450100/450107"},{"code":450108,"name":"良庆","fullname":"良庆区","level":"district","filename":"450000/450100/450108"},{"code":450109,"name":"邕宁","fullname":"邕宁区","level":"district","filename":"450000/450100/450109"},{"code":450110,"name":"武鸣","fullname":"武鸣区","level":"district","filename":"450000/450100/450110"},{"code":450123,"name":"隆安","fullname":"隆安县","level":"district","filename":"450000/450100/450123"},{"code":450124,"name":"马山","fullname":"马山县","level":"district","filename":"450000/450100/450124"},{"code":450125,"name":"上林","fullname":"上林县","level":"district","filename":"450000/450100/450125"},{"code":450126,"name":"宾阳","fullname":"宾阳县","level":"district","filename":"450000/450100/450126"},{"code":450127,"name":"横州","fullname":"横州市","level":"district","filename":"450000/450100/450127"}]},{"code":450200,"name":"柳州","fullname":"柳州市","level":"city","filename":"450000/450200","children":[{"code":450202,"name":"城中","fullname":"城中区","level":"district","filename":"450000/450200/450202"},{"code":450203,"name":"鱼峰","fullname":"鱼峰区","level":"district","filename":"450000/450200/450203"},{"code":450204,"name":"柳南","fullname":"柳南区","level":"district","filename":"450000/450200/450204"},{"code":450205,"name":"柳北","fullname":"柳北区","level":"district","filename":"450000/450200/450205"},{"code":450206,"name":"柳江","fullname":"柳江区","level":"district","filename":"450000/450200/450206"},{"code":450222,"name":"柳城","fullname":"柳城县","level":"district","filename":"450000/450200/450222"},{"code":450223,"name":"鹿寨","fullname":"鹿寨县","level":"district","filename":"450000/450200/450223"},{"code":450224,"name":"融安","fullname":"融安县","level":"district","filename":"450000/450200/450224"},{"code":450225,"name":"融水","fullname":"融水苗族自治县","level":"district","filename":"450000/450200/450225"},{"code":450226,"name":"三江","fullname":"三江侗族自治县","level":"district","filename":"450000/450200/450226"}]},{"code":450300,"name":"桂林","fullname":"桂林市","level":"city","filename":"450000/450300","children":[{"code":450302,"name":"秀峰","fullname":"秀峰区","level":"district","filename":"450000/450300/450302"},{"code":450303,"name":"叠彩","fullname":"叠彩区","level":"district","filename":"450000/450300/450303"},{"code":450304,"name":"象山","fullname":"象山区","level":"district","filename":"450000/450300/450304"},{"code":450305,"name":"七星","fullname":"七星区","level":"district","filename":"450000/450300/450305"},{"code":450311,"name":"雁山","fullname":"雁山区","level":"district","filename":"450000/450300/450311"},{"code":450312,"name":"临桂","fullname":"临桂区","level":"district","filename":"450000/450300/450312"},{"code":450321,"name":"阳朔","fullname":"阳朔县","level":"district","filename":"450000/450300/450321"},{"code":450323,"name":"灵川","fullname":"灵川县","level":"district","filename":"450000/450300/450323"},{"code":450324,"name":"全州","fullname":"全州县","level":"district","filename":"450000/450300/450324"},{"code":450325,"name":"兴安","fullname":"兴安县","level":"district","filename":"450000/450300/450325"},{"code":450326,"name":"永福","fullname":"永福县","level":"district","filename":"450000/450300/450326"},{"code":450327,"name":"灌阳","fullname":"灌阳县","level":"district","filename":"450000/450300/450327"},{"code":450328,"name":"龙胜","fullname":"龙胜各族自治县","level":"district","filename":"450000/450300/450328"},{"code":450329,"name":"资源","fullname":"资源县","level":"district","filename":"450000/450300/450329"},{"code":450330,"name":"平乐","fullname":"平乐县","level":"district","filename":"450000/450300/450330"},{"code":450332,"name":"恭城","fullname":"恭城瑶族自治县","level":"district","filename":"450000/450300/450332"},{"code":450381,"name":"荔浦","fullname":"荔浦市","level":"district","filename":"450000/450300/450381"}]},{"code":450400,"name":"梧州","fullname":"梧州市","level":"city","filename":"450000/450400","children":[{"code":450403,"name":"万秀","fullname":"万秀区","level":"district","filename":"450000/450400/450403"},{"code":450405,"name":"长洲","fullname":"长洲区","level":"district","filename":"450000/450400/450405"},{"code":450406,"name":"龙圩","fullname":"龙圩区","level":"district","filename":"450000/450400/450406"},{"code":450421,"name":"苍梧","fullname":"苍梧县","level":"district","filename":"450000/450400/450421"},{"code":450422,"name":"藤县","fullname":"藤县","level":"district","filename":"450000/450400/450422"},{"code":450423,"name":"蒙山","fullname":"蒙山县","level":"district","filename":"450000/450400/450423"},{"code":450481,"name":"岑溪","fullname":"岑溪市","level":"district","filename":"450000/450400/450481"}]},{"code":450500,"name":"北海","fullname":"北海市","level":"city","filename":"450000/450500","children":[{"code":450502,"name":"海城","fullname":"海城区","level":"district","filename":"450000/450500/450502"},{"code":450503,"name":"银海","fullname":"银海区","level":"district","filename":"450000/450500/450503"},{"code":450512,"name":"铁山港","fullname":"铁山港区","level":"district","filename":"450000/450500/450512"},{"code":450521,"name":"合浦","fullname":"合浦县","level":"district","filename":"450000/450500/450521"}]},{"code":450600,"name":"防城港","fullname":"防城港市","level":"city","filename":"450000/450600","children":[{"code":450602,"name":"港口","fullname":"港口区","level":"district","filename":"450000/450600/450602"},{"code":450603,"name":"防城","fullname":"防城区","level":"district","filename":"450000/450600/450603"},{"code":450621,"name":"上思","fullname":"上思县","level":"district","filename":"450000/450600/450621"},{"code":450681,"name":"东兴","fullname":"东兴市","level":"district","filename":"450000/450600/450681"}]},{"code":450700,"name":"钦州","fullname":"钦州市","level":"city","filename":"450000/450700","children":[{"code":450702,"name":"钦南","fullname":"钦南区","level":"district","filename":"450000/450700/450702"},{"code":450703,"name":"钦北","fullname":"钦北区","level":"district","filename":"450000/450700/450703"},{"code":450721,"name":"灵山","fullname":"灵山县","level":"district","filename":"450000/450700/450721"},{"code":450722,"name":"浦北","fullname":"浦北县","level":"district","filename":"450000/450700/450722"}]},{"code":450800,"name":"贵港","fullname":"贵港市","level":"city","filename":"450000/450800","children":[{"code":450802,"name":"港北","fullname":"港北区","level":"district","filename":"450000/450800/450802"},{"code":450803,"name":"港南","fullname":"港南区","level":"district","filename":"450000/450800/450803"},{"code":450804,"name":"覃塘","fullname":"覃塘区","level":"district","filename":"450000/450800/450804"},{"code":450821,"name":"平南","fullname":"平南县","level":"district","filename":"450000/450800/450821"},{"code":450881,"name":"桂平","fullname":"桂平市","level":"district","filename":"450000/450800/450881"}]},{"code":450900,"name":"玉林","fullname":"玉林市","level":"city","filename":"450000/450900","children":[{"code":450902,"name":"玉州","fullname":"玉州区","level":"district","filename":"450000/450900/450902"},{"code":450903,"name":"福绵","fullname":"福绵区","level":"district","filename":"450000/450900/450903"},{"code":450921,"name":"容县","fullname":"容县","level":"district","filename":"450000/450900/450921"},{"code":450922,"name":"陆川","fullname":"陆川县","level":"district","filename":"450000/450900/450922"},{"code":450923,"name":"博白","fullname":"博白县","level":"district","filename":"450000/450900/450923"},{"code":450924,"name":"兴业","fullname":"兴业县","level":"district","filename":"450000/450900/450924"},{"code":450981,"name":"北流","fullname":"北流市","level":"district","filename":"450000/450900/450981"}]},{"code":451000,"name":"百色","fullname":"百色市","level":"city","filename":"450000/451000","children":[{"code":451002,"name":"右江","fullname":"右江区","level":"district","filename":"450000/451000/451002"},{"code":451003,"name":"田阳","fullname":"田阳区","level":"district","filename":"450000/451000/451003"},{"code":451022,"name":"田东","fullname":"田东县","level":"district","filename":"450000/451000/451022"},{"code":451024,"name":"德保","fullname":"德保县","level":"district","filename":"450000/451000/451024"},{"code":451026,"name":"那坡","fullname":"那坡县","level":"district","filename":"450000/451000/451026"},{"code":451027,"name":"凌云","fullname":"凌云县","level":"district","filename":"450000/451000/451027"},{"code":451028,"name":"乐业","fullname":"乐业县","level":"district","filename":"450000/451000/451028"},{"code":451029,"name":"田林","fullname":"田林县","level":"district","filename":"450000/451000/451029"},{"code":451030,"name":"西林","fullname":"西林县","level":"district","filename":"450000/451000/451030"},{"code":451031,"name":"隆林","fullname":"隆林各族自治县","level":"district","filename":"450000/451000/451031"},{"code":451081,"name":"靖西","fullname":"靖西市","level":"district","filename":"450000/451000/451081"},{"code":451082,"name":"平果","fullname":"平果市","level":"district","filename":"450000/451000/451082"}]},{"code":451100,"name":"贺州","fullname":"贺州市","level":"city","filename":"450000/451100","children":[{"code":451102,"name":"八步","fullname":"八步区","level":"district","filename":"450000/451100/451102"},{"code":451103,"name":"平桂","fullname":"平桂区","level":"district","filename":"450000/451100/451103"},{"code":451121,"name":"昭平","fullname":"昭平县","level":"district","filename":"450000/451100/451121"},{"code":451122,"name":"钟山","fullname":"钟山县","level":"district","filename":"450000/451100/451122"},{"code":451123,"name":"富川","fullname":"富川瑶族自治县","level":"district","filename":"450000/451100/451123"}]},{"code":451200,"name":"河池","fullname":"河池市","level":"city","filename":"450000/451200","children":[{"code":451202,"name":"金城江","fullname":"金城江区","level":"district","filename":"450000/451200/451202"},{"code":451203,"name":"宜州","fullname":"宜州区","level":"district","filename":"450000/451200/451203"},{"code":451221,"name":"南丹","fullname":"南丹县","level":"district","filename":"450000/451200/451221"},{"code":451222,"name":"天峨","fullname":"天峨县","level":"district","filename":"450000/451200/451222"},{"code":451223,"name":"凤山","fullname":"凤山县","level":"district","filename":"450000/451200/451223"},{"code":451224,"name":"东兰","fullname":"东兰县","level":"district","filename":"450000/451200/451224"},{"code":451225,"name":"罗城","fullname":"罗城仫佬族自治县","level":"district","filename":"450000/451200/451225"},{"code":451226,"name":"环江","fullname":"环江毛南族自治县","level":"district","filename":"450000/451200/451226"},{"code":451227,"name":"巴马","fullname":"巴马瑶族自治县","level":"district","filename":"450000/451200/451227"},{"code":451228,"name":"都安","fullname":"都安瑶族自治县","level":"district","filename":"450000/451200/451228"},{"code":451229,"name":"大化","fullname":"大化瑶族自治县","level":"district","filename":"450000/451200/451229"}]},{"code":451300,"name":"来宾","fullname":"来宾市","level":"city","filename":"450000/451300","children":[{"code":451302,"name":"兴宾","fullname":"兴宾区","level":"district","filename":"450000/451300/451302"},{"code":451321,"name":"忻城","fullname":"忻城县","level":"district","filename":"450000/451300/451321"},{"code":451322,"name":"象州","fullname":"象州县","level":"district","filename":"450000/451300/451322"},{"code":451323,"name":"武宣","fullname":"武宣县","level":"district","filename":"450000/451300/451323"},{"code":451324,"name":"金秀","fullname":"金秀瑶族自治县","level":"district","filename":"450000/451300/451324"},{"code":451381,"name":"合山","fullname":"合山市","level":"district","filename":"450000/451300/451381"}]},{"code":451400,"name":"崇左","fullname":"崇左市","level":"city","filename":"450000/451400","children":[{"code":451402,"name":"江州","fullname":"江州区","level":"district","filename":"450000/451400/451402"},{"code":451421,"name":"扶绥","fullname":"扶绥县","level":"district","filename":"450000/451400/451421"},{"code":451422,"name":"宁明","fullname":"宁明县","level":"district","filename":"450000/451400/451422"},{"code":451423,"name":"龙州","fullname":"龙州县","level":"district","filename":"450000/451400/451423"},{"code":451424,"name":"大新","fullname":"大新县","level":"district","filename":"450000/451400/451424"},{"code":451425,"name":"天等","fullname":"天等县","level":"district","filename":"450000/451400/451425"},{"code":451481,"name":"凭祥","fullname":"凭祥市","level":"district","filename":"450000/451400/451481"}]}]},{"code":460000,"name":"海南","fullname":"海南省","level":"province","filename":"460000","children":[{"code":460100,"name":"海口","fullname":"海口市","level":"city","filename":"460000/460100","children":[{"code":460105,"name":"秀英","fullname":"秀英区","level":"district","filename":"460000/460100/460105"},{"code":460106,"name":"龙华","fullname":"龙华区","level":"district","filename":"460000/460100/460106"},{"code":460107,"name":"琼山","fullname":"琼山区","level":"district","filename":"460000/460100/460107"},{"code":460108,"name":"美兰","fullname":"美兰区","level":"district","filename":"460000/460100/460108"}]},{"code":460200,"name":"三亚","fullname":"三亚市","level":"city","filename":"460000/460200","children":[{"code":460202,"name":"海棠","fullname":"海棠区","level":"district","filename":"460000/460200/460202"},{"code":460203,"name":"吉阳","fullname":"吉阳区","level":"district","filename":"460000/460200/460203"},{"code":460204,"name":"天涯","fullname":"天涯区","level":"district","filename":"460000/460200/460204"},{"code":460205,"name":"崖州","fullname":"崖州区","level":"district","filename":"460000/460200/460205"}]},{"code":460300,"name":"三沙","fullname":"三沙市","level":"city","filename":"460000/460300","children":[{"code":460301,"name":"西沙","fullname":"西沙区","level":"district","filename":"460000/460300/460301"},{"code":460302,"name":"南沙","fullname":"南沙区","level":"district","filename":"460000/460300/460302"}]},{"code":460400,"name":"儋州","fullname":"儋州市","level":"city","filename":"460000/460400"},{"code":469001,"name":"五指山","fullname":"五指山市","level":"city","filename":"460000/469001"},{"code":469002,"name":"琼海","fullname":"琼海市","level":"city","filename":"460000/469002"},{"code":469005,"name":"文昌","fullname":"文昌市","level":"city","filename":"460000/469005"},{"code":469006,"name":"万宁","fullname":"万宁市","level":"city","filename":"460000/469006"},{"code":469007,"name":"东方","fullname":"东方市","level":"city","filename":"460000/469007"},{"code":469021,"name":"定安县","fullname":"定安县","level":"city","filename":"460000/469021"},{"code":469022,"name":"屯昌县","fullname":"屯昌县","level":"city","filename":"460000/469022"},{"code":469023,"name":"澄迈县","fullname":"澄迈县","level":"city","filename":"460000/469023"},{"code":469024,"name":"临高县","fullname":"临高县","level":"city","filename":"460000/469024"},{"code":469025,"name":"白沙","fullname":"白沙黎族自治县","level":"city","filename":"460000/469025"},{"code":469026,"name":"昌江","fullname":"昌江黎族自治县","level":"city","filename":"460000/469026"},{"code":469027,"name":"乐东","fullname":"乐东黎族自治县","level":"city","filename":"460000/469027"},{"code":469028,"name":"陵水","fullname":"陵水黎族自治县","level":"city","filename":"460000/469028"},{"code":469029,"name":"保亭","fullname":"保亭黎族苗族自治县","level":"city","filename":"460000/469029"},{"code":469030,"name":"琼中","fullname":"琼中黎族苗族自治县","level":"city","filename":"460000/469030"}]},{"code":500000,"name":"重庆","fullname":"重庆市","level":"province","filename":"500000","children":[{"code":500101,"name":"万州","fullname":"万州区","level":"district","filename":"500000/500101"},{"code":500102,"name":"涪陵","fullname":"涪陵区","level":"district","filename":"500000/500102"},{"code":500103,"name":"渝中","fullname":"渝中区","level":"district","filename":"500000/500103"},{"code":500104,"name":"大渡口","fullname":"大渡口区","level":"district","filename":"500000/500104"},{"code":500105,"name":"江北","fullname":"江北区","level":"district","filename":"500000/500105"},{"code":500106,"name":"沙坪坝","fullname":"沙坪坝区","level":"district","filename":"500000/500106"},{"code":500107,"name":"九龙坡","fullname":"九龙坡区","level":"district","filename":"500000/500107"},{"code":500108,"name":"南岸","fullname":"南岸区","level":"district","filename":"500000/500108"},{"code":500109,"name":"北碚","fullname":"北碚区","level":"district","filename":"500000/500109"},{"code":500110,"name":"綦江","fullname":"綦江区","level":"district","filename":"500000/500110"},{"code":500111,"name":"大足","fullname":"大足区","level":"district","filename":"500000/500111"},{"code":500112,"name":"渝北","fullname":"渝北区","level":"district","filename":"500000/500112"},{"code":500113,"name":"巴南","fullname":"巴南区","level":"district","filename":"500000/500113"},{"code":500114,"name":"黔江","fullname":"黔江区","level":"district","filename":"500000/500114"},{"code":500115,"name":"长寿","fullname":"长寿区","level":"district","filename":"500000/500115"},{"code":500116,"name":"江津","fullname":"江津区","level":"district","filename":"500000/500116"},{"code":500117,"name":"合川","fullname":"合川区","level":"district","filename":"500000/500117"},{"code":500118,"name":"永川","fullname":"永川区","level":"district","filename":"500000/500118"},{"code":500119,"name":"南川","fullname":"南川区","level":"district","filename":"500000/500119"},{"code":500120,"name":"璧山","fullname":"璧山区","level":"district","filename":"500000/500120"},{"code":500151,"name":"铜梁","fullname":"铜梁区","level":"district","filename":"500000/500151"},{"code":500152,"name":"潼南","fullname":"潼南区","level":"district","filename":"500000/500152"},{"code":500153,"name":"荣昌","fullname":"荣昌区","level":"district","filename":"500000/500153"},{"code":500154,"name":"开州","fullname":"开州区","level":"district","filename":"500000/500154"},{"code":500155,"name":"梁平","fullname":"梁平区","level":"district","filename":"500000/500155"},{"code":500156,"name":"武隆","fullname":"武隆区","level":"district","filename":"500000/500156"},{"code":500229,"name":"城口","fullname":"城口县","level":"district","filename":"500000/500229"},{"code":500230,"name":"丰都","fullname":"丰都县","level":"district","filename":"500000/500230"},{"code":500231,"name":"垫江","fullname":"垫江县","level":"district","filename":"500000/500231"},{"code":500233,"name":"忠县","fullname":"忠县","level":"district","filename":"500000/500233"},{"code":500235,"name":"云阳","fullname":"云阳县","level":"district","filename":"500000/500235"},{"code":500236,"name":"奉节","fullname":"奉节县","level":"district","filename":"500000/500236"},{"code":500237,"name":"巫山","fullname":"巫山县","level":"district","filename":"500000/500237"},{"code":500238,"name":"巫溪","fullname":"巫溪县","level":"district","filename":"500000/500238"},{"code":500240,"name":"石柱","fullname":"石柱土家族自治县","level":"district","filename":"500000/500240"},{"code":500241,"name":"秀山","fullname":"秀山土家族苗族自治县","level":"district","filename":"500000/500241"},{"code":500242,"name":"酉阳","fullname":"酉阳土家族苗族自治县","level":"district","filename":"500000/500242"},{"code":500243,"name":"彭水","fullname":"彭水苗族土家族自治县","level":"district","filename":"500000/500243"}]},{"code":510000,"name":"四川","fullname":"四川省","level":"province","filename":"510000","children":[{"code":510100,"name":"成都","fullname":"成都市","level":"city","filename":"510000/510100","children":[{"code":510104,"name":"锦江","fullname":"锦江区","level":"district","filename":"510000/510100/510104"},{"code":510105,"name":"青羊","fullname":"青羊区","level":"district","filename":"510000/510100/510105"},{"code":510106,"name":"金牛","fullname":"金牛区","level":"district","filename":"510000/510100/510106"},{"code":510107,"name":"武侯","fullname":"武侯区","level":"district","filename":"510000/510100/510107"},{"code":510108,"name":"成华","fullname":"成华区","level":"district","filename":"510000/510100/510108"},{"code":510112,"name":"龙泉驿","fullname":"龙泉驿区","level":"district","filename":"510000/510100/510112"},{"code":510113,"name":"青白江","fullname":"青白江区","level":"district","filename":"510000/510100/510113"},{"code":510114,"name":"新都","fullname":"新都区","level":"district","filename":"510000/510100/510114"},{"code":510115,"name":"温江","fullname":"温江区","level":"district","filename":"510000/510100/510115"},{"code":510116,"name":"双流","fullname":"双流区","level":"district","filename":"510000/510100/510116"},{"code":510117,"name":"郫都","fullname":"郫都区","level":"district","filename":"510000/510100/510117"},{"code":510118,"name":"新津","fullname":"新津区","level":"district","filename":"510000/510100/510118"},{"code":510121,"name":"金堂","fullname":"金堂县","level":"district","filename":"510000/510100/510121"},{"code":510129,"name":"大邑","fullname":"大邑县","level":"district","filename":"510000/510100/510129"},{"code":510131,"name":"蒲江","fullname":"蒲江县","level":"district","filename":"510000/510100/510131"},{"code":510181,"name":"都江堰","fullname":"都江堰市","level":"district","filename":"510000/510100/510181"},{"code":510182,"name":"彭州","fullname":"彭州市","level":"district","filename":"510000/510100/510182"},{"code":510183,"name":"邛崃","fullname":"邛崃市","level":"district","filename":"510000/510100/510183"},{"code":510184,"name":"崇州","fullname":"崇州市","level":"district","filename":"510000/510100/510184"},{"code":510185,"name":"简阳","fullname":"简阳市","level":"district","filename":"510000/510100/510185"}]},{"code":510300,"name":"自贡","fullname":"自贡市","level":"city","filename":"510000/510300","children":[{"code":510302,"name":"自流井","fullname":"自流井区","level":"district","filename":"510000/510300/510302"},{"code":510303,"name":"贡井","fullname":"贡井区","level":"district","filename":"510000/510300/510303"},{"code":510304,"name":"大安","fullname":"大安区","level":"district","filename":"510000/510300/510304"},{"code":510311,"name":"沿滩","fullname":"沿滩区","level":"district","filename":"510000/510300/510311"},{"code":510321,"name":"荣县","fullname":"荣县","level":"district","filename":"510000/510300/510321"},{"code":510322,"name":"富顺","fullname":"富顺县","level":"district","filename":"510000/510300/510322"}]},{"code":510400,"name":"攀枝花","fullname":"攀枝花市","level":"city","filename":"510000/510400","children":[{"code":510402,"name":"东区","fullname":"东区","level":"district","filename":"510000/510400/510402"},{"code":510403,"name":"西区","fullname":"西区","level":"district","filename":"510000/510400/510403"},{"code":510411,"name":"仁和","fullname":"仁和区","level":"district","filename":"510000/510400/510411"},{"code":510421,"name":"米易","fullname":"米易县","level":"district","filename":"510000/510400/510421"},{"code":510422,"name":"盐边","fullname":"盐边县","level":"district","filename":"510000/510400/510422"}]},{"code":510500,"name":"泸州","fullname":"泸州市","level":"city","filename":"510000/510500","children":[{"code":510502,"name":"江阳","fullname":"江阳区","level":"district","filename":"510000/510500/510502"},{"code":510503,"name":"纳溪","fullname":"纳溪区","level":"district","filename":"510000/510500/510503"},{"code":510504,"name":"龙马潭","fullname":"龙马潭区","level":"district","filename":"510000/510500/510504"},{"code":510521,"name":"泸县","fullname":"泸县","level":"district","filename":"510000/510500/510521"},{"code":510522,"name":"合江","fullname":"合江县","level":"district","filename":"510000/510500/510522"},{"code":510524,"name":"叙永","fullname":"叙永县","level":"district","filename":"510000/510500/510524"},{"code":510525,"name":"古蔺","fullname":"古蔺县","level":"district","filename":"510000/510500/510525"}]},{"code":510600,"name":"德阳","fullname":"德阳市","level":"city","filename":"510000/510600","children":[{"code":510603,"name":"旌阳","fullname":"旌阳区","level":"district","filename":"510000/510600/510603"},{"code":510604,"name":"罗江","fullname":"罗江区","level":"district","filename":"510000/510600/510604"},{"code":510623,"name":"中江","fullname":"中江县","level":"district","filename":"510000/510600/510623"},{"code":510681,"name":"广汉","fullname":"广汉市","level":"district","filename":"510000/510600/510681"},{"code":510682,"name":"什邡","fullname":"什邡市","level":"district","filename":"510000/510600/510682"},{"code":510683,"name":"绵竹","fullname":"绵竹市","level":"district","filename":"510000/510600/510683"}]},{"code":510700,"name":"绵阳","fullname":"绵阳市","level":"city","filename":"510000/510700","children":[{"code":510703,"name":"涪城","fullname":"涪城区","level":"district","filename":"510000/510700/510703"},{"code":510704,"name":"游仙","fullname":"游仙区","level":"district","filename":"510000/510700/510704"},{"code":510705,"name":"安州","fullname":"安州区","level":"district","filename":"510000/510700/510705"},{"code":510722,"name":"三台","fullname":"三台县","level":"district","filename":"510000/510700/510722"},{"code":510723,"name":"盐亭","fullname":"盐亭县","level":"district","filename":"510000/510700/510723"},{"code":510725,"name":"梓潼","fullname":"梓潼县","level":"district","filename":"510000/510700/510725"},{"code":510726,"name":"北川","fullname":"北川羌族自治县","level":"district","filename":"510000/510700/510726"},{"code":510727,"name":"平武","fullname":"平武县","level":"district","filename":"510000/510700/510727"},{"code":510781,"name":"江油","fullname":"江油市","level":"district","filename":"510000/510700/510781"}]},{"code":510800,"name":"广元","fullname":"广元市","level":"city","filename":"510000/510800","children":[{"code":510802,"name":"利州","fullname":"利州区","level":"district","filename":"510000/510800/510802"},{"code":510811,"name":"昭化","fullname":"昭化区","level":"district","filename":"510000/510800/510811"},{"code":510812,"name":"朝天","fullname":"朝天区","level":"district","filename":"510000/510800/510812"},{"code":510821,"name":"旺苍","fullname":"旺苍县","level":"district","filename":"510000/510800/510821"},{"code":510822,"name":"青川","fullname":"青川县","level":"district","filename":"510000/510800/510822"},{"code":510823,"name":"剑阁","fullname":"剑阁县","level":"district","filename":"510000/510800/510823"},{"code":510824,"name":"苍溪","fullname":"苍溪县","level":"district","filename":"510000/510800/510824"}]},{"code":510900,"name":"遂宁","fullname":"遂宁市","level":"city","filename":"510000/510900","children":[{"code":510903,"name":"船山","fullname":"船山区","level":"district","filename":"510000/510900/510903"},{"code":510904,"name":"安居","fullname":"安居区","level":"district","filename":"510000/510900/510904"},{"code":510921,"name":"蓬溪","fullname":"蓬溪县","level":"district","filename":"510000/510900/510921"},{"code":510923,"name":"大英","fullname":"大英县","level":"district","filename":"510000/510900/510923"},{"code":510981,"name":"射洪","fullname":"射洪市","level":"district","filename":"510000/510900/510981"}]},{"code":511000,"name":"内江","fullname":"内江市","level":"city","filename":"510000/511000","children":[{"code":511002,"name":"市中区","fullname":"市中区","level":"district","filename":"510000/511000/511002"},{"code":511011,"name":"东兴","fullname":"东兴区","level":"district","filename":"510000/511000/511011"},{"code":511024,"name":"威远","fullname":"威远县","level":"district","filename":"510000/511000/511024"},{"code":511025,"name":"资中","fullname":"资中县","level":"district","filename":"510000/511000/511025"},{"code":511083,"name":"隆昌","fullname":"隆昌市","level":"district","filename":"510000/511000/511083"}]},{"code":511100,"name":"乐山","fullname":"乐山市","level":"city","filename":"510000/511100","children":[{"code":511102,"name":"市中区","fullname":"市中区","level":"district","filename":"510000/511100/511102"},{"code":511111,"name":"沙湾","fullname":"沙湾区","level":"district","filename":"510000/511100/511111"},{"code":511112,"name":"五通桥","fullname":"五通桥区","level":"district","filename":"510000/511100/511112"},{"code":511113,"name":"金口河","fullname":"金口河区","level":"district","filename":"510000/511100/511113"},{"code":511123,"name":"犍为","fullname":"犍为县","level":"district","filename":"510000/511100/511123"},{"code":511124,"name":"井研","fullname":"井研县","level":"district","filename":"510000/511100/511124"},{"code":511126,"name":"夹江","fullname":"夹江县","level":"district","filename":"510000/511100/511126"},{"code":511129,"name":"沐川","fullname":"沐川县","level":"district","filename":"510000/511100/511129"},{"code":511132,"name":"峨边","fullname":"峨边彝族自治县","level":"district","filename":"510000/511100/511132"},{"code":511133,"name":"马边","fullname":"马边彝族自治县","level":"district","filename":"510000/511100/511133"},{"code":511181,"name":"峨眉山","fullname":"峨眉山市","level":"district","filename":"510000/511100/511181"}]},{"code":511300,"name":"南充","fullname":"南充市","level":"city","filename":"510000/511300","children":[{"code":511302,"name":"顺庆","fullname":"顺庆区","level":"district","filename":"510000/511300/511302"},{"code":511303,"name":"高坪","fullname":"高坪区","level":"district","filename":"510000/511300/511303"},{"code":511304,"name":"嘉陵","fullname":"嘉陵区","level":"district","filename":"510000/511300/511304"},{"code":511321,"name":"南部","fullname":"南部县","level":"district","filename":"510000/511300/511321"},{"code":511322,"name":"营山","fullname":"营山县","level":"district","filename":"510000/511300/511322"},{"code":511323,"name":"蓬安","fullname":"蓬安县","level":"district","filename":"510000/511300/511323"},{"code":511324,"name":"仪陇","fullname":"仪陇县","level":"district","filename":"510000/511300/511324"},{"code":511325,"name":"西充","fullname":"西充县","level":"district","filename":"510000/511300/511325"},{"code":511381,"name":"阆中","fullname":"阆中市","level":"district","filename":"510000/511300/511381"}]},{"code":511400,"name":"眉山","fullname":"眉山市","level":"city","filename":"510000/511400","children":[{"code":511402,"name":"东坡","fullname":"东坡区","level":"district","filename":"510000/511400/511402"},{"code":511403,"name":"彭山","fullname":"彭山区","level":"district","filename":"510000/511400/511403"},{"code":511421,"name":"仁寿","fullname":"仁寿县","level":"district","filename":"510000/511400/511421"},{"code":511423,"name":"洪雅","fullname":"洪雅县","level":"district","filename":"510000/511400/511423"},{"code":511424,"name":"丹棱","fullname":"丹棱县","level":"district","filename":"510000/511400/511424"},{"code":511425,"name":"青神","fullname":"青神县","level":"district","filename":"510000/511400/511425"}]},{"code":511500,"name":"宜宾","fullname":"宜宾市","level":"city","filename":"510000/511500","children":[{"code":511502,"name":"翠屏","fullname":"翠屏区","level":"district","filename":"510000/511500/511502"},{"code":511503,"name":"南溪","fullname":"南溪区","level":"district","filename":"510000/511500/511503"},{"code":511504,"name":"叙州","fullname":"叙州区","level":"district","filename":"510000/511500/511504"},{"code":511523,"name":"江安","fullname":"江安县","level":"district","filename":"510000/511500/511523"},{"code":511524,"name":"长宁","fullname":"长宁县","level":"district","filename":"510000/511500/511524"},{"code":511525,"name":"高县","fullname":"高县","level":"district","filename":"510000/511500/511525"},{"code":511526,"name":"珙县","fullname":"珙县","level":"district","filename":"510000/511500/511526"},{"code":511527,"name":"筠连","fullname":"筠连县","level":"district","filename":"510000/511500/511527"},{"code":511528,"name":"兴文","fullname":"兴文县","level":"district","filename":"510000/511500/511528"},{"code":511529,"name":"屏山","fullname":"屏山县","level":"district","filename":"510000/511500/511529"}]},{"code":511600,"name":"广安","fullname":"广安市","level":"city","filename":"510000/511600","children":[{"code":511602,"name":"广安","fullname":"广安区","level":"district","filename":"510000/511600/511602"},{"code":511603,"name":"前锋","fullname":"前锋区","level":"district","filename":"510000/511600/511603"},{"code":511621,"name":"岳池","fullname":"岳池县","level":"district","filename":"510000/511600/511621"},{"code":511622,"name":"武胜","fullname":"武胜县","level":"district","filename":"510000/511600/511622"},{"code":511623,"name":"邻水","fullname":"邻水县","level":"district","filename":"510000/511600/511623"},{"code":511681,"name":"华蓥","fullname":"华蓥市","level":"district","filename":"510000/511600/511681"}]},{"code":511700,"name":"达州","fullname":"达州市","level":"city","filename":"510000/511700","children":[{"code":511702,"name":"通川","fullname":"通川区","level":"district","filename":"510000/511700/511702"},{"code":511703,"name":"达川","fullname":"达川区","level":"district","filename":"510000/511700/511703"},{"code":511722,"name":"宣汉","fullname":"宣汉县","level":"district","filename":"510000/511700/511722"},{"code":511723,"name":"开江","fullname":"开江县","level":"district","filename":"510000/511700/511723"},{"code":511724,"name":"大竹","fullname":"大竹县","level":"district","filename":"510000/511700/511724"},{"code":511725,"name":"渠县","fullname":"渠县","level":"district","filename":"510000/511700/511725"},{"code":511781,"name":"万源","fullname":"万源市","level":"district","filename":"510000/511700/511781"}]},{"code":511800,"name":"雅安","fullname":"雅安市","level":"city","filename":"510000/511800","children":[{"code":511802,"name":"雨城","fullname":"雨城区","level":"district","filename":"510000/511800/511802"},{"code":511803,"name":"名山","fullname":"名山区","level":"district","filename":"510000/511800/511803"},{"code":511822,"name":"荥经","fullname":"荥经县","level":"district","filename":"510000/511800/511822"},{"code":511823,"name":"汉源","fullname":"汉源县","level":"district","filename":"510000/511800/511823"},{"code":511824,"name":"石棉","fullname":"石棉县","level":"district","filename":"510000/511800/511824"},{"code":511825,"name":"天全","fullname":"天全县","level":"district","filename":"510000/511800/511825"},{"code":511826,"name":"芦山","fullname":"芦山县","level":"district","filename":"510000/511800/511826"},{"code":511827,"name":"宝兴","fullname":"宝兴县","level":"district","filename":"510000/511800/511827"}]},{"code":511900,"name":"巴中","fullname":"巴中市","level":"city","filename":"510000/511900","children":[{"code":511902,"name":"巴州","fullname":"巴州区","level":"district","filename":"510000/511900/511902"},{"code":511903,"name":"恩阳","fullname":"恩阳区","level":"district","filename":"510000/511900/511903"},{"code":511921,"name":"通江","fullname":"通江县","level":"district","filename":"510000/511900/511921"},{"code":511922,"name":"南江","fullname":"南江县","level":"district","filename":"510000/511900/511922"},{"code":511923,"name":"平昌","fullname":"平昌县","level":"district","filename":"510000/511900/511923"}]},{"code":512000,"name":"资阳","fullname":"资阳市","level":"city","filename":"510000/512000","children":[{"code":512002,"name":"雁江","fullname":"雁江区","level":"district","filename":"510000/512000/512002"},{"code":512021,"name":"安岳","fullname":"安岳县","level":"district","filename":"510000/512000/512021"},{"code":512022,"name":"乐至","fullname":"乐至县","level":"district","filename":"510000/512000/512022"}]},{"code":513200,"name":"阿坝","fullname":"阿坝藏族羌族自治州","level":"city","filename":"510000/513200","children":[{"code":513201,"name":"马尔康","fullname":"马尔康市","level":"district","filename":"510000/513200/513201"},{"code":513221,"name":"汶川","fullname":"汶川县","level":"district","filename":"510000/513200/513221"},{"code":513222,"name":"理县","fullname":"理县","level":"district","filename":"510000/513200/513222"},{"code":513223,"name":"茂县","fullname":"茂县","level":"district","filename":"510000/513200/513223"},{"code":513224,"name":"松潘","fullname":"松潘县","level":"district","filename":"510000/513200/513224"},{"code":513225,"name":"九寨沟","fullname":"九寨沟县","level":"district","filename":"510000/513200/513225"},{"code":513226,"name":"金川","fullname":"金川县","level":"district","filename":"510000/513200/513226"},{"code":513227,"name":"小金","fullname":"小金县","level":"district","filename":"510000/513200/513227"},{"code":513228,"name":"黑水","fullname":"黑水县","level":"district","filename":"510000/513200/513228"},{"code":513230,"name":"壤塘","fullname":"壤塘县","level":"district","filename":"510000/513200/513230"},{"code":513231,"name":"阿坝","fullname":"阿坝县","level":"district","filename":"510000/513200/513231"},{"code":513232,"name":"若尔盖","fullname":"若尔盖县","level":"district","filename":"510000/513200/513232"},{"code":513233,"name":"红原","fullname":"红原县","level":"district","filename":"510000/513200/513233"}]},{"code":513300,"name":"甘孜","fullname":"甘孜藏族自治州","level":"city","filename":"510000/513300","children":[{"code":513301,"name":"康定","fullname":"康定市","level":"district","filename":"510000/513300/513301"},{"code":513322,"name":"泸定","fullname":"泸定县","level":"district","filename":"510000/513300/513322"},{"code":513323,"name":"丹巴","fullname":"丹巴县","level":"district","filename":"510000/513300/513323"},{"code":513324,"name":"九龙","fullname":"九龙县","level":"district","filename":"510000/513300/513324"},{"code":513325,"name":"雅江","fullname":"雅江县","level":"district","filename":"510000/513300/513325"},{"code":513326,"name":"道孚","fullname":"道孚县","level":"district","filename":"510000/513300/513326"},{"code":513327,"name":"炉霍","fullname":"炉霍县","level":"district","filename":"510000/513300/513327"},{"code":513328,"name":"甘孜","fullname":"甘孜县","level":"district","filename":"510000/513300/513328"},{"code":513329,"name":"新龙","fullname":"新龙县","level":"district","filename":"510000/513300/513329"},{"code":513330,"name":"德格","fullname":"德格县","level":"district","filename":"510000/513300/513330"},{"code":513331,"name":"白玉","fullname":"白玉县","level":"district","filename":"510000/513300/513331"},{"code":513332,"name":"石渠","fullname":"石渠县","level":"district","filename":"510000/513300/513332"},{"code":513333,"name":"色达","fullname":"色达县","level":"district","filename":"510000/513300/513333"},{"code":513334,"name":"理塘","fullname":"理塘县","level":"district","filename":"510000/513300/513334"},{"code":513335,"name":"巴塘","fullname":"巴塘县","level":"district","filename":"510000/513300/513335"},{"code":513336,"name":"乡城","fullname":"乡城县","level":"district","filename":"510000/513300/513336"},{"code":513337,"name":"稻城","fullname":"稻城县","level":"district","filename":"510000/513300/513337"},{"code":513338,"name":"得荣","fullname":"得荣县","level":"district","filename":"510000/513300/513338"}]},{"code":513400,"name":"凉山","fullname":"凉山彝族自治州","level":"city","filename":"510000/513400","children":[{"code":513401,"name":"西昌","fullname":"西昌市","level":"district","filename":"510000/513400/513401"},{"code":513422,"name":"木里","fullname":"木里藏族自治县","level":"district","filename":"510000/513400/513422"},{"code":513423,"name":"盐源","fullname":"盐源县","level":"district","filename":"510000/513400/513423"},{"code":513424,"name":"德昌","fullname":"德昌县","level":"district","filename":"510000/513400/513424"},{"code":513425,"name":"会理","fullname":"会理市","level":"district","filename":"510000/513400/513425"},{"code":513426,"name":"会东","fullname":"会东县","level":"district","filename":"510000/513400/513426"},{"code":513427,"name":"宁南","fullname":"宁南县","level":"district","filename":"510000/513400/513427"},{"code":513428,"name":"普格","fullname":"普格县","level":"district","filename":"510000/513400/513428"},{"code":513429,"name":"布拖","fullname":"布拖县","level":"district","filename":"510000/513400/513429"},{"code":513430,"name":"金阳","fullname":"金阳县","level":"district","filename":"510000/513400/513430"},{"code":513431,"name":"昭觉","fullname":"昭觉县","level":"district","filename":"510000/513400/513431"},{"code":513432,"name":"喜德","fullname":"喜德县","level":"district","filename":"510000/513400/513432"},{"code":513433,"name":"冕宁","fullname":"冕宁县","level":"district","filename":"510000/513400/513433"},{"code":513434,"name":"越西","fullname":"越西县","level":"district","filename":"510000/513400/513434"},{"code":513435,"name":"甘洛","fullname":"甘洛县","level":"district","filename":"510000/513400/513435"},{"code":513436,"name":"美姑","fullname":"美姑县","level":"district","filename":"510000/513400/513436"},{"code":513437,"name":"雷波","fullname":"雷波县","level":"district","filename":"510000/513400/513437"}]}]},{"code":520000,"name":"贵州","fullname":"贵州省","level":"province","filename":"520000","children":[{"code":520100,"name":"贵阳","fullname":"贵阳市","level":"city","filename":"520000/520100","children":[{"code":520102,"name":"南明","fullname":"南明区","level":"district","filename":"520000/520100/520102"},{"code":520103,"name":"云岩","fullname":"云岩区","level":"district","filename":"520000/520100/520103"},{"code":520111,"name":"花溪","fullname":"花溪区","level":"district","filename":"520000/520100/520111"},{"code":520112,"name":"乌当","fullname":"乌当区","level":"district","filename":"520000/520100/520112"},{"code":520113,"name":"白云","fullname":"白云区","level":"district","filename":"520000/520100/520113"},{"code":520115,"name":"观山湖","fullname":"观山湖区","level":"district","filename":"520000/520100/520115"},{"code":520121,"name":"开阳","fullname":"开阳县","level":"district","filename":"520000/520100/520121"},{"code":520122,"name":"息烽","fullname":"息烽县","level":"district","filename":"520000/520100/520122"},{"code":520123,"name":"修文","fullname":"修文县","level":"district","filename":"520000/520100/520123"},{"code":520181,"name":"清镇","fullname":"清镇市","level":"district","filename":"520000/520100/520181"}]},{"code":520200,"name":"六盘水","fullname":"六盘水市","level":"city","filename":"520000/520200","children":[{"code":520201,"name":"钟山","fullname":"钟山区","level":"district","filename":"520000/520200/520201"},{"code":520203,"name":"六枝特","fullname":"六枝特区","level":"district","filename":"520000/520200/520203"},{"code":520221,"name":"水城","fullname":"水城区","level":"district","filename":"520000/520200/520221"},{"code":520281,"name":"盘州","fullname":"盘州市","level":"district","filename":"520000/520200/520281"}]},{"code":520300,"name":"遵义","fullname":"遵义市","level":"city","filename":"520000/520300","children":[{"code":520302,"name":"红花岗","fullname":"红花岗区","level":"district","filename":"520000/520300/520302"},{"code":520303,"name":"汇川","fullname":"汇川区","level":"district","filename":"520000/520300/520303"},{"code":520304,"name":"播州","fullname":"播州区","level":"district","filename":"520000/520300/520304"},{"code":520322,"name":"桐梓","fullname":"桐梓县","level":"district","filename":"520000/520300/520322"},{"code":520323,"name":"绥阳","fullname":"绥阳县","level":"district","filename":"520000/520300/520323"},{"code":520324,"name":"正安","fullname":"正安县","level":"district","filename":"520000/520300/520324"},{"code":520325,"name":"道真","fullname":"道真仡佬族苗族自治县","level":"district","filename":"520000/520300/520325"},{"code":520326,"name":"务川","fullname":"务川仡佬族苗族自治县","level":"district","filename":"520000/520300/520326"},{"code":520327,"name":"凤冈","fullname":"凤冈县","level":"district","filename":"520000/520300/520327"},{"code":520328,"name":"湄潭","fullname":"湄潭县","level":"district","filename":"520000/520300/520328"},{"code":520329,"name":"余庆","fullname":"余庆县","level":"district","filename":"520000/520300/520329"},{"code":520330,"name":"习水","fullname":"习水县","level":"district","filename":"520000/520300/520330"},{"code":520381,"name":"赤水","fullname":"赤水市","level":"district","filename":"520000/520300/520381"},{"code":520382,"name":"仁怀","fullname":"仁怀市","level":"district","filename":"520000/520300/520382"}]},{"code":520400,"name":"安顺","fullname":"安顺市","level":"city","filename":"520000/520400","children":[{"code":520402,"name":"西秀","fullname":"西秀区","level":"district","filename":"520000/520400/520402"},{"code":520403,"name":"平坝","fullname":"平坝区","level":"district","filename":"520000/520400/520403"},{"code":520422,"name":"普定","fullname":"普定县","level":"district","filename":"520000/520400/520422"},{"code":520423,"name":"镇宁","fullname":"镇宁布依族苗族自治县","level":"district","filename":"520000/520400/520423"},{"code":520424,"name":"关岭","fullname":"关岭布依族苗族自治县","level":"district","filename":"520000/520400/520424"},{"code":520425,"name":"紫云","fullname":"紫云苗族布依族自治县","level":"district","filename":"520000/520400/520425"}]},{"code":520500,"name":"毕节","fullname":"毕节市","level":"city","filename":"520000/520500","children":[{"code":520502,"name":"七星关","fullname":"七星关区","level":"district","filename":"520000/520500/520502"},{"code":520521,"name":"大方","fullname":"大方县","level":"district","filename":"520000/520500/520521"},{"code":520522,"name":"黔西","fullname":"黔西市","level":"district","filename":"520000/520500/520522"},{"code":520523,"name":"金沙","fullname":"金沙县","level":"district","filename":"520000/520500/520523"},{"code":520524,"name":"织金","fullname":"织金县","level":"district","filename":"520000/520500/520524"},{"code":520525,"name":"纳雍","fullname":"纳雍县","level":"district","filename":"520000/520500/520525"},{"code":520526,"name":"威宁","fullname":"威宁彝族回族苗族自治县","level":"district","filename":"520000/520500/520526"},{"code":520527,"name":"赫章","fullname":"赫章县","level":"district","filename":"520000/520500/520527"}]},{"code":520600,"name":"铜仁","fullname":"铜仁市","level":"city","filename":"520000/520600","children":[{"code":520602,"name":"碧江","fullname":"碧江区","level":"district","filename":"520000/520600/520602"},{"code":520603,"name":"万山","fullname":"万山区","level":"district","filename":"520000/520600/520603"},{"code":520621,"name":"江口","fullname":"江口县","level":"district","filename":"520000/520600/520621"},{"code":520622,"name":"玉屏","fullname":"玉屏侗族自治县","level":"district","filename":"520000/520600/520622"},{"code":520623,"name":"石阡","fullname":"石阡县","level":"district","filename":"520000/520600/520623"},{"code":520624,"name":"思南","fullname":"思南县","level":"district","filename":"520000/520600/520624"},{"code":520625,"name":"印江","fullname":"印江土家族苗族自治县","level":"district","filename":"520000/520600/520625"},{"code":520626,"name":"德江","fullname":"德江县","level":"district","filename":"520000/520600/520626"},{"code":520627,"name":"沿河","fullname":"沿河土家族自治县","level":"district","filename":"520000/520600/520627"},{"code":520628,"name":"松桃","fullname":"松桃苗族自治县","level":"district","filename":"520000/520600/520628"}]},{"code":522300,"name":"黔西南","fullname":"黔西南布依族苗族自治州","level":"city","filename":"520000/522300","children":[{"code":522301,"name":"兴义","fullname":"兴义市","level":"district","filename":"520000/522300/522301"},{"code":522302,"name":"兴仁","fullname":"兴仁市","level":"district","filename":"520000/522300/522302"},{"code":522323,"name":"普安","fullname":"普安县","level":"district","filename":"520000/522300/522323"},{"code":522324,"name":"晴隆","fullname":"晴隆县","level":"district","filename":"520000/522300/522324"},{"code":522325,"name":"贞丰","fullname":"贞丰县","level":"district","filename":"520000/522300/522325"},{"code":522326,"name":"望谟","fullname":"望谟县","level":"district","filename":"520000/522300/522326"},{"code":522327,"name":"册亨","fullname":"册亨县","level":"district","filename":"520000/522300/522327"},{"code":522328,"name":"安龙","fullname":"安龙县","level":"district","filename":"520000/522300/522328"}]},{"code":522600,"name":"黔东南","fullname":"黔东南苗族侗族自治州","level":"city","filename":"520000/522600","children":[{"code":522601,"name":"凯里","fullname":"凯里市","level":"district","filename":"520000/522600/522601"},{"code":522622,"name":"黄平","fullname":"黄平县","level":"district","filename":"520000/522600/522622"},{"code":522623,"name":"施秉","fullname":"施秉县","level":"district","filename":"520000/522600/522623"},{"code":522624,"name":"三穗","fullname":"三穗县","level":"district","filename":"520000/522600/522624"},{"code":522625,"name":"镇远","fullname":"镇远县","level":"district","filename":"520000/522600/522625"},{"code":522626,"name":"岑巩","fullname":"岑巩县","level":"district","filename":"520000/522600/522626"},{"code":522627,"name":"天柱","fullname":"天柱县","level":"district","filename":"520000/522600/522627"},{"code":522628,"name":"锦屏","fullname":"锦屏县","level":"district","filename":"520000/522600/522628"},{"code":522629,"name":"剑河","fullname":"剑河县","level":"district","filename":"520000/522600/522629"},{"code":522630,"name":"台江","fullname":"台江县","level":"district","filename":"520000/522600/522630"},{"code":522631,"name":"黎平","fullname":"黎平县","level":"district","filename":"520000/522600/522631"},{"code":522632,"name":"榕江","fullname":"榕江县","level":"district","filename":"520000/522600/522632"},{"code":522633,"name":"从江","fullname":"从江县","level":"district","filename":"520000/522600/522633"},{"code":522634,"name":"雷山","fullname":"雷山县","level":"district","filename":"520000/522600/522634"},{"code":522635,"name":"麻江","fullname":"麻江县","level":"district","filename":"520000/522600/522635"},{"code":522636,"name":"丹寨","fullname":"丹寨县","level":"district","filename":"520000/522600/522636"}]},{"code":522700,"name":"黔南","fullname":"黔南布依族苗族自治州","level":"city","filename":"520000/522700","children":[{"code":522701,"name":"都匀","fullname":"都匀市","level":"district","filename":"520000/522700/522701"},{"code":522702,"name":"福泉","fullname":"福泉市","level":"district","filename":"520000/522700/522702"},{"code":522722,"name":"荔波","fullname":"荔波县","level":"district","filename":"520000/522700/522722"},{"code":522723,"name":"贵定","fullname":"贵定县","level":"district","filename":"520000/522700/522723"},{"code":522725,"name":"瓮安","fullname":"瓮安县","level":"district","filename":"520000/522700/522725"},{"code":522726,"name":"独山","fullname":"独山县","level":"district","filename":"520000/522700/522726"},{"code":522727,"name":"平塘","fullname":"平塘县","level":"district","filename":"520000/522700/522727"},{"code":522728,"name":"罗甸","fullname":"罗甸县","level":"district","filename":"520000/522700/522728"},{"code":522729,"name":"长顺","fullname":"长顺县","level":"district","filename":"520000/522700/522729"},{"code":522730,"name":"龙里","fullname":"龙里县","level":"district","filename":"520000/522700/522730"},{"code":522731,"name":"惠水","fullname":"惠水县","level":"district","filename":"520000/522700/522731"},{"code":522732,"name":"三都","fullname":"三都水族自治县","level":"district","filename":"520000/522700/522732"}]}]},{"code":530000,"name":"云南","fullname":"云南省","level":"province","filename":"530000","children":[{"code":530100,"name":"昆明","fullname":"昆明市","level":"city","filename":"530000/530100","children":[{"code":530102,"name":"五华","fullname":"五华区","level":"district","filename":"530000/530100/530102"},{"code":530103,"name":"盘龙","fullname":"盘龙区","level":"district","filename":"530000/530100/530103"},{"code":530111,"name":"官渡","fullname":"官渡区","level":"district","filename":"530000/530100/530111"},{"code":530112,"name":"西山","fullname":"西山区","level":"district","filename":"530000/530100/530112"},{"code":530113,"name":"东川","fullname":"东川区","level":"district","filename":"530000/530100/530113"},{"code":530114,"name":"呈贡","fullname":"呈贡区","level":"district","filename":"530000/530100/530114"},{"code":530115,"name":"晋宁","fullname":"晋宁区","level":"district","filename":"530000/530100/530115"},{"code":530124,"name":"富民","fullname":"富民县","level":"district","filename":"530000/530100/530124"},{"code":530125,"name":"宜良","fullname":"宜良县","level":"district","filename":"530000/530100/530125"},{"code":530126,"name":"石林","fullname":"石林彝族自治县","level":"district","filename":"530000/530100/530126"},{"code":530127,"name":"嵩明","fullname":"嵩明县","level":"district","filename":"530000/530100/530127"},{"code":530128,"name":"禄劝","fullname":"禄劝彝族苗族自治县","level":"district","filename":"530000/530100/530128"},{"code":530129,"name":"寻甸","fullname":"寻甸回族彝族自治县","level":"district","filename":"530000/530100/530129"},{"code":530181,"name":"安宁","fullname":"安宁市","level":"district","filename":"530000/530100/530181"}]},{"code":530300,"name":"曲靖","fullname":"曲靖市","level":"city","filename":"530000/530300","children":[{"code":530302,"name":"麒麟","fullname":"麒麟区","level":"district","filename":"530000/530300/530302"},{"code":530303,"name":"沾益","fullname":"沾益区","level":"district","filename":"530000/530300/530303"},{"code":530304,"name":"马龙","fullname":"马龙区","level":"district","filename":"530000/530300/530304"},{"code":530322,"name":"陆良","fullname":"陆良县","level":"district","filename":"530000/530300/530322"},{"code":530323,"name":"师宗","fullname":"师宗县","level":"district","filename":"530000/530300/530323"},{"code":530324,"name":"罗平","fullname":"罗平县","level":"district","filename":"530000/530300/530324"},{"code":530325,"name":"富源","fullname":"富源县","level":"district","filename":"530000/530300/530325"},{"code":530326,"name":"会泽","fullname":"会泽县","level":"district","filename":"530000/530300/530326"},{"code":530381,"name":"宣威","fullname":"宣威市","level":"district","filename":"530000/530300/530381"}]},{"code":530400,"name":"玉溪","fullname":"玉溪市","level":"city","filename":"530000/530400","children":[{"code":530402,"name":"红塔","fullname":"红塔区","level":"district","filename":"530000/530400/530402"},{"code":530403,"name":"江川","fullname":"江川区","level":"district","filename":"530000/530400/530403"},{"code":530423,"name":"通海","fullname":"通海县","level":"district","filename":"530000/530400/530423"},{"code":530424,"name":"华宁","fullname":"华宁县","level":"district","filename":"530000/530400/530424"},{"code":530425,"name":"易门","fullname":"易门县","level":"district","filename":"530000/530400/530425"},{"code":530426,"name":"峨山","fullname":"峨山彝族自治县","level":"district","filename":"530000/530400/530426"},{"code":530427,"name":"新平","fullname":"新平彝族傣族自治县","level":"district","filename":"530000/530400/530427"},{"code":530428,"name":"元江","fullname":"元江哈尼族彝族傣族自治县","level":"district","filename":"530000/530400/530428"},{"code":530481,"name":"澄江","fullname":"澄江市","level":"district","filename":"530000/530400/530481"}]},{"code":530500,"name":"保山","fullname":"保山市","level":"city","filename":"530000/530500","children":[{"code":530502,"name":"隆阳","fullname":"隆阳区","level":"district","filename":"530000/530500/530502"},{"code":530521,"name":"施甸","fullname":"施甸县","level":"district","filename":"530000/530500/530521"},{"code":530523,"name":"龙陵","fullname":"龙陵县","level":"district","filename":"530000/530500/530523"},{"code":530524,"name":"昌宁","fullname":"昌宁县","level":"district","filename":"530000/530500/530524"},{"code":530581,"name":"腾冲","fullname":"腾冲市","level":"district","filename":"530000/530500/530581"}]},{"code":530600,"name":"昭通","fullname":"昭通市","level":"city","filename":"530000/530600","children":[{"code":530602,"name":"昭阳","fullname":"昭阳区","level":"district","filename":"530000/530600/530602"},{"code":530621,"name":"鲁甸","fullname":"鲁甸县","level":"district","filename":"530000/530600/530621"},{"code":530622,"name":"巧家","fullname":"巧家县","level":"district","filename":"530000/530600/530622"},{"code":530623,"name":"盐津","fullname":"盐津县","level":"district","filename":"530000/530600/530623"},{"code":530624,"name":"大关","fullname":"大关县","level":"district","filename":"530000/530600/530624"},{"code":530625,"name":"永善","fullname":"永善县","level":"district","filename":"530000/530600/530625"},{"code":530626,"name":"绥江","fullname":"绥江县","level":"district","filename":"530000/530600/530626"},{"code":530627,"name":"镇雄","fullname":"镇雄县","level":"district","filename":"530000/530600/530627"},{"code":530628,"name":"彝良","fullname":"彝良县","level":"district","filename":"530000/530600/530628"},{"code":530629,"name":"威信","fullname":"威信县","level":"district","filename":"530000/530600/530629"},{"code":530681,"name":"水富","fullname":"水富市","level":"district","filename":"530000/530600/530681"}]},{"code":530700,"name":"丽江","fullname":"丽江市","level":"city","filename":"530000/530700","children":[{"code":530702,"name":"古城","fullname":"古城区","level":"district","filename":"530000/530700/530702"},{"code":530721,"name":"玉龙","fullname":"玉龙纳西族自治县","level":"district","filename":"530000/530700/530721"},{"code":530722,"name":"永胜","fullname":"永胜县","level":"district","filename":"530000/530700/530722"},{"code":530723,"name":"华坪","fullname":"华坪县","level":"district","filename":"530000/530700/530723"},{"code":530724,"name":"宁蒗","fullname":"宁蒗彝族自治县","level":"district","filename":"530000/530700/530724"}]},{"code":530800,"name":"普洱","fullname":"普洱市","level":"city","filename":"530000/530800","children":[{"code":530802,"name":"思茅","fullname":"思茅区","level":"district","filename":"530000/530800/530802"},{"code":530821,"name":"宁洱","fullname":"宁洱哈尼族彝族自治县","level":"district","filename":"530000/530800/530821"},{"code":530822,"name":"墨江","fullname":"墨江哈尼族自治县","level":"district","filename":"530000/530800/530822"},{"code":530823,"name":"景东","fullname":"景东彝族自治县","level":"district","filename":"530000/530800/530823"},{"code":530824,"name":"景谷","fullname":"景谷傣族彝族自治县","level":"district","filename":"530000/530800/530824"},{"code":530825,"name":"镇沅","fullname":"镇沅彝族哈尼族拉祜族自治县","level":"district","filename":"530000/530800/530825"},{"code":530826,"name":"江城","fullname":"江城哈尼族彝族自治县","level":"district","filename":"530000/530800/530826"},{"code":530827,"name":"孟连","fullname":"孟连傣族拉祜族佤族自治县","level":"district","filename":"530000/530800/530827"},{"code":530828,"name":"澜沧","fullname":"澜沧拉祜族自治县","level":"district","filename":"530000/530800/530828"},{"code":530829,"name":"西盟","fullname":"西盟佤族自治县","level":"district","filename":"530000/530800/530829"}]},{"code":530900,"name":"临沧","fullname":"临沧市","level":"city","filename":"530000/530900","children":[{"code":530902,"name":"临翔","fullname":"临翔区","level":"district","filename":"530000/530900/530902"},{"code":530921,"name":"凤庆","fullname":"凤庆县","level":"district","filename":"530000/530900/530921"},{"code":530922,"name":"云县","fullname":"云县","level":"district","filename":"530000/530900/530922"},{"code":530923,"name":"永德","fullname":"永德县","level":"district","filename":"530000/530900/530923"},{"code":530924,"name":"镇康","fullname":"镇康县","level":"district","filename":"530000/530900/530924"},{"code":530925,"name":"双江","fullname":"双江拉祜族佤族布朗族傣族自治县","level":"district","filename":"530000/530900/530925"},{"code":530926,"name":"耿马","fullname":"耿马傣族佤族自治县","level":"district","filename":"530000/530900/530926"},{"code":530927,"name":"沧源","fullname":"沧源佤族自治县","level":"district","filename":"530000/530900/530927"}]},{"code":532300,"name":"楚雄","fullname":"楚雄彝族自治州","level":"city","filename":"530000/532300","children":[{"code":532301,"name":"楚雄","fullname":"楚雄市","level":"district","filename":"530000/532300/532301"},{"code":532322,"name":"双柏","fullname":"双柏县","level":"district","filename":"530000/532300/532322"},{"code":532323,"name":"牟定","fullname":"牟定县","level":"district","filename":"530000/532300/532323"},{"code":532324,"name":"南华","fullname":"南华县","level":"district","filename":"530000/532300/532324"},{"code":532325,"name":"姚安","fullname":"姚安县","level":"district","filename":"530000/532300/532325"},{"code":532326,"name":"大姚","fullname":"大姚县","level":"district","filename":"530000/532300/532326"},{"code":532327,"name":"永仁","fullname":"永仁县","level":"district","filename":"530000/532300/532327"},{"code":532328,"name":"元谋","fullname":"元谋县","level":"district","filename":"530000/532300/532328"},{"code":532329,"name":"武定","fullname":"武定县","level":"district","filename":"530000/532300/532329"},{"code":532331,"name":"禄丰","fullname":"禄丰市","level":"district","filename":"530000/532300/532331"}]},{"code":532500,"name":"红河","fullname":"红河哈尼族彝族自治州","level":"city","filename":"530000/532500","children":[{"code":532501,"name":"个旧","fullname":"个旧市","level":"district","filename":"530000/532500/532501"},{"code":532502,"name":"开远","fullname":"开远市","level":"district","filename":"530000/532500/532502"},{"code":532503,"name":"蒙自","fullname":"蒙自市","level":"district","filename":"530000/532500/532503"},{"code":532504,"name":"弥勒","fullname":"弥勒市","level":"district","filename":"530000/532500/532504"},{"code":532523,"name":"屏边","fullname":"屏边苗族自治县","level":"district","filename":"530000/532500/532523"},{"code":532524,"name":"建水","fullname":"建水县","level":"district","filename":"530000/532500/532524"},{"code":532525,"name":"石屏","fullname":"石屏县","level":"district","filename":"530000/532500/532525"},{"code":532527,"name":"泸西","fullname":"泸西县","level":"district","filename":"530000/532500/532527"},{"code":532528,"name":"元阳","fullname":"元阳县","level":"district","filename":"530000/532500/532528"},{"code":532529,"name":"红河","fullname":"红河县","level":"district","filename":"530000/532500/532529"},{"code":532530,"name":"金平","fullname":"金平苗族瑶族傣族自治县","level":"district","filename":"530000/532500/532530"},{"code":532531,"name":"绿春","fullname":"绿春县","level":"district","filename":"530000/532500/532531"},{"code":532532,"name":"河口","fullname":"河口瑶族自治县","level":"district","filename":"530000/532500/532532"}]},{"code":532600,"name":"文山","fullname":"文山壮族苗族自治州","level":"city","filename":"530000/532600","children":[{"code":532601,"name":"文山","fullname":"文山市","level":"district","filename":"530000/532600/532601"},{"code":532622,"name":"砚山","fullname":"砚山县","level":"district","filename":"530000/532600/532622"},{"code":532623,"name":"西畴","fullname":"西畴县","level":"district","filename":"530000/532600/532623"},{"code":532624,"name":"麻栗坡","fullname":"麻栗坡县","level":"district","filename":"530000/532600/532624"},{"code":532625,"name":"马关","fullname":"马关县","level":"district","filename":"530000/532600/532625"},{"code":532626,"name":"丘北","fullname":"丘北县","level":"district","filename":"530000/532600/532626"},{"code":532627,"name":"广南","fullname":"广南县","level":"district","filename":"530000/532600/532627"},{"code":532628,"name":"富宁","fullname":"富宁县","level":"district","filename":"530000/532600/532628"}]},{"code":532800,"name":"西双版纳","fullname":"西双版纳傣族自治州","level":"city","filename":"530000/532800","children":[{"code":532801,"name":"景洪","fullname":"景洪市","level":"district","filename":"530000/532800/532801"},{"code":532822,"name":"勐海","fullname":"勐海县","level":"district","filename":"530000/532800/532822"},{"code":532823,"name":"勐腊","fullname":"勐腊县","level":"district","filename":"530000/532800/532823"}]},{"code":532900,"name":"大理","fullname":"大理白族自治州","level":"city","filename":"530000/532900","children":[{"code":532901,"name":"大理","fullname":"大理市","level":"district","filename":"530000/532900/532901"},{"code":532922,"name":"漾濞","fullname":"漾濞彝族自治县","level":"district","filename":"530000/532900/532922"},{"code":532923,"name":"祥云","fullname":"祥云县","level":"district","filename":"530000/532900/532923"},{"code":532924,"name":"宾川","fullname":"宾川县","level":"district","filename":"530000/532900/532924"},{"code":532925,"name":"弥渡","fullname":"弥渡县","level":"district","filename":"530000/532900/532925"},{"code":532926,"name":"南涧","fullname":"南涧彝族自治县","level":"district","filename":"530000/532900/532926"},{"code":532927,"name":"巍山","fullname":"巍山彝族回族自治县","level":"district","filename":"530000/532900/532927"},{"code":532928,"name":"永平","fullname":"永平县","level":"district","filename":"530000/532900/532928"},{"code":532929,"name":"云龙","fullname":"云龙县","level":"district","filename":"530000/532900/532929"},{"code":532930,"name":"洱源","fullname":"洱源县","level":"district","filename":"530000/532900/532930"},{"code":532931,"name":"剑川","fullname":"剑川县","level":"district","filename":"530000/532900/532931"},{"code":532932,"name":"鹤庆","fullname":"鹤庆县","level":"district","filename":"530000/532900/532932"}]},{"code":533100,"name":"德宏","fullname":"德宏傣族景颇族自治州","level":"city","filename":"530000/533100","children":[{"code":533102,"name":"瑞丽","fullname":"瑞丽市","level":"district","filename":"530000/533100/533102"},{"code":533103,"name":"芒市","fullname":"芒市","level":"district","filename":"530000/533100/533103"},{"code":533122,"name":"梁河","fullname":"梁河县","level":"district","filename":"530000/533100/533122"},{"code":533123,"name":"盈江","fullname":"盈江县","level":"district","filename":"530000/533100/533123"},{"code":533124,"name":"陇川","fullname":"陇川县","level":"district","filename":"530000/533100/533124"}]},{"code":533300,"name":"怒江","fullname":"怒江傈僳族自治州","level":"city","filename":"530000/533300","children":[{"code":533301,"name":"泸水","fullname":"泸水市","level":"district","filename":"530000/533300/533301"},{"code":533323,"name":"福贡","fullname":"福贡县","level":"district","filename":"530000/533300/533323"},{"code":533324,"name":"贡山","fullname":"贡山独龙族怒族自治县","level":"district","filename":"530000/533300/533324"},{"code":533325,"name":"兰坪","fullname":"兰坪白族普米族自治县","level":"district","filename":"530000/533300/533325"}]},{"code":533400,"name":"迪庆","fullname":"迪庆藏族自治州","level":"city","filename":"530000/533400","children":[{"code":533401,"name":"香格里拉","fullname":"香格里拉市","level":"district","filename":"530000/533400/533401"},{"code":533422,"name":"德钦","fullname":"德钦县","level":"district","filename":"530000/533400/533422"},{"code":533423,"name":"维西","fullname":"维西傈僳族自治县","level":"district","filename":"530000/533400/533423"}]}]},{"code":540000,"name":"西藏","fullname":"西藏自治区","level":"province","filename":"540000","children":[{"code":540100,"name":"拉萨","fullname":"拉萨市","level":"city","filename":"540000/540100","children":[{"code":540102,"name":"城关","fullname":"城关区","level":"district","filename":"540000/540100/540102"},{"code":540103,"name":"堆龙德庆","fullname":"堆龙德庆区","level":"district","filename":"540000/540100/540103"},{"code":540104,"name":"达孜","fullname":"达孜区","level":"district","filename":"540000/540100/540104"},{"code":540121,"name":"林周","fullname":"林周县","level":"district","filename":"540000/540100/540121"},{"code":540122,"name":"当雄","fullname":"当雄县","level":"district","filename":"540000/540100/540122"},{"code":540123,"name":"尼木","fullname":"尼木县","level":"district","filename":"540000/540100/540123"},{"code":540124,"name":"曲水","fullname":"曲水县","level":"district","filename":"540000/540100/540124"},{"code":540127,"name":"墨竹工卡","fullname":"墨竹工卡县","level":"district","filename":"540000/540100/540127"}]},{"code":540200,"name":"日喀则","fullname":"日喀则市","level":"city","filename":"540000/540200","children":[{"code":540202,"name":"桑珠孜","fullname":"桑珠孜区","level":"district","filename":"540000/540200/540202"},{"code":540221,"name":"南木林","fullname":"南木林县","level":"district","filename":"540000/540200/540221"},{"code":540222,"name":"江孜","fullname":"江孜县","level":"district","filename":"540000/540200/540222"},{"code":540223,"name":"定日","fullname":"定日县","level":"district","filename":"540000/540200/540223"},{"code":540224,"name":"萨迦","fullname":"萨迦县","level":"district","filename":"540000/540200/540224"},{"code":540225,"name":"拉孜","fullname":"拉孜县","level":"district","filename":"540000/540200/540225"},{"code":540226,"name":"昂仁","fullname":"昂仁县","level":"district","filename":"540000/540200/540226"},{"code":540227,"name":"谢通门","fullname":"谢通门县","level":"district","filename":"540000/540200/540227"},{"code":540228,"name":"白朗","fullname":"白朗县","level":"district","filename":"540000/540200/540228"},{"code":540229,"name":"仁布","fullname":"仁布县","level":"district","filename":"540000/540200/540229"},{"code":540230,"name":"康马","fullname":"康马县","level":"district","filename":"540000/540200/540230"},{"code":540231,"name":"定结","fullname":"定结县","level":"district","filename":"540000/540200/540231"},{"code":540232,"name":"仲巴","fullname":"仲巴县","level":"district","filename":"540000/540200/540232"},{"code":540233,"name":"亚东","fullname":"亚东县","level":"district","filename":"540000/540200/540233"},{"code":540234,"name":"吉隆","fullname":"吉隆县","level":"district","filename":"540000/540200/540234"},{"code":540235,"name":"聂拉木","fullname":"聂拉木县","level":"district","filename":"540000/540200/540235"},{"code":540236,"name":"萨嘎","fullname":"萨嘎县","level":"district","filename":"540000/540200/540236"},{"code":540237,"name":"岗巴","fullname":"岗巴县","level":"district","filename":"540000/540200/540237"}]},{"code":540300,"name":"昌都","fullname":"昌都市","level":"city","filename":"540000/540300","children":[{"code":540302,"name":"卡若","fullname":"卡若区","level":"district","filename":"540000/540300/540302"},{"code":540321,"name":"江达","fullname":"江达县","level":"district","filename":"540000/540300/540321"},{"code":540322,"name":"贡觉","fullname":"贡觉县","level":"district","filename":"540000/540300/540322"},{"code":540323,"name":"类乌齐","fullname":"类乌齐县","level":"district","filename":"540000/540300/540323"},{"code":540324,"name":"丁青","fullname":"丁青县","level":"district","filename":"540000/540300/540324"},{"code":540325,"name":"察雅","fullname":"察雅县","level":"district","filename":"540000/540300/540325"},{"code":540326,"name":"八宿","fullname":"八宿县","level":"district","filename":"540000/540300/540326"},{"code":540327,"name":"左贡","fullname":"左贡县","level":"district","filename":"540000/540300/540327"},{"code":540328,"name":"芒康","fullname":"芒康县","level":"district","filename":"540000/540300/540328"},{"code":540329,"name":"洛隆","fullname":"洛隆县","level":"district","filename":"540000/540300/540329"},{"code":540330,"name":"边坝","fullname":"边坝县","level":"district","filename":"540000/540300/540330"}]},{"code":540400,"name":"林芝","fullname":"林芝市","level":"city","filename":"540000/540400","children":[{"code":540402,"name":"巴宜","fullname":"巴宜区","level":"district","filename":"540000/540400/540402"},{"code":540421,"name":"工布江达","fullname":"工布江达县","level":"district","filename":"540000/540400/540421"},{"code":540422,"name":"米林","fullname":"米林县","level":"district","filename":"540000/540400/540422"},{"code":540423,"name":"墨脱","fullname":"墨脱县","level":"district","filename":"540000/540400/540423"},{"code":540424,"name":"波密","fullname":"波密县","level":"district","filename":"540000/540400/540424"},{"code":540425,"name":"察隅","fullname":"察隅县","level":"district","filename":"540000/540400/540425"},{"code":540426,"name":"朗县","fullname":"朗县","level":"district","filename":"540000/540400/540426"}]},{"code":540500,"name":"山南","fullname":"山南市","level":"city","filename":"540000/540500","children":[{"code":540502,"name":"乃东","fullname":"乃东区","level":"district","filename":"540000/540500/540502"},{"code":540521,"name":"扎囊","fullname":"扎囊县","level":"district","filename":"540000/540500/540521"},{"code":540522,"name":"贡嘎","fullname":"贡嘎县","level":"district","filename":"540000/540500/540522"},{"code":540523,"name":"桑日","fullname":"桑日县","level":"district","filename":"540000/540500/540523"},{"code":540524,"name":"琼结","fullname":"琼结县","level":"district","filename":"540000/540500/540524"},{"code":540525,"name":"曲松","fullname":"曲松县","level":"district","filename":"540000/540500/540525"},{"code":540526,"name":"措美","fullname":"措美县","level":"district","filename":"540000/540500/540526"},{"code":540527,"name":"洛扎","fullname":"洛扎县","level":"district","filename":"540000/540500/540527"},{"code":540528,"name":"加查","fullname":"加查县","level":"district","filename":"540000/540500/540528"},{"code":540529,"name":"隆子","fullname":"隆子县","level":"district","filename":"540000/540500/540529"},{"code":540530,"name":"错那","fullname":"错那县","level":"district","filename":"540000/540500/540530"},{"code":540531,"name":"浪卡子","fullname":"浪卡子县","level":"district","filename":"540000/540500/540531"}]},{"code":540600,"name":"那曲","fullname":"那曲市","level":"city","filename":"540000/540600","children":[{"code":540602,"name":"色尼","fullname":"色尼区","level":"district","filename":"540000/540600/540602"},{"code":540621,"name":"嘉黎","fullname":"嘉黎县","level":"district","filename":"540000/540600/540621"},{"code":540622,"name":"比如","fullname":"比如县","level":"district","filename":"540000/540600/540622"},{"code":540623,"name":"聂荣","fullname":"聂荣县","level":"district","filename":"540000/540600/540623"},{"code":540624,"name":"安多","fullname":"安多县","level":"district","filename":"540000/540600/540624"},{"code":540625,"name":"申扎","fullname":"申扎县","level":"district","filename":"540000/540600/540625"},{"code":540626,"name":"索县","fullname":"索县","level":"district","filename":"540000/540600/540626"},{"code":540627,"name":"班戈","fullname":"班戈县","level":"district","filename":"540000/540600/540627"},{"code":540628,"name":"巴青","fullname":"巴青县","level":"district","filename":"540000/540600/540628"},{"code":540629,"name":"尼玛","fullname":"尼玛县","level":"district","filename":"540000/540600/540629"},{"code":540630,"name":"双湖","fullname":"双湖县","level":"district","filename":"540000/540600/540630"}]},{"code":542500,"name":"阿里","fullname":"阿里地区","level":"city","filename":"540000/542500","children":[{"code":542521,"name":"普兰","fullname":"普兰县","level":"district","filename":"540000/542500/542521"},{"code":542522,"name":"札达","fullname":"札达县","level":"district","filename":"540000/542500/542522"},{"code":542523,"name":"噶尔","fullname":"噶尔县","level":"district","filename":"540000/542500/542523"},{"code":542524,"name":"日土","fullname":"日土县","level":"district","filename":"540000/542500/542524"},{"code":542525,"name":"革吉","fullname":"革吉县","level":"district","filename":"540000/542500/542525"},{"code":542526,"name":"改则","fullname":"改则县","level":"district","filename":"540000/542500/542526"},{"code":542527,"name":"措勤","fullname":"措勤县","level":"district","filename":"540000/542500/542527"}]}]},{"code":610000,"name":"陕西","fullname":"陕西省","level":"province","filename":"610000","children":[{"code":610100,"name":"西安","fullname":"西安市","level":"city","filename":"610000/610100","children":[{"code":610102,"name":"新城","fullname":"新城区","level":"district","filename":"610000/610100/610102"},{"code":610103,"name":"碑林","fullname":"碑林区","level":"district","filename":"610000/610100/610103"},{"code":610104,"name":"莲湖","fullname":"莲湖区","level":"district","filename":"610000/610100/610104"},{"code":610111,"name":"灞桥","fullname":"灞桥区","level":"district","filename":"610000/610100/610111"},{"code":610112,"name":"未央","fullname":"未央区","level":"district","filename":"610000/610100/610112"},{"code":610113,"name":"雁塔","fullname":"雁塔区","level":"district","filename":"610000/610100/610113"},{"code":610114,"name":"阎良","fullname":"阎良区","level":"district","filename":"610000/610100/610114"},{"code":610115,"name":"临潼","fullname":"临潼区","level":"district","filename":"610000/610100/610115"},{"code":610116,"name":"长安","fullname":"长安区","level":"district","filename":"610000/610100/610116"},{"code":610117,"name":"高陵","fullname":"高陵区","level":"district","filename":"610000/610100/610117"},{"code":610118,"name":"鄠邑","fullname":"鄠邑区","level":"district","filename":"610000/610100/610118"},{"code":610122,"name":"蓝田","fullname":"蓝田县","level":"district","filename":"610000/610100/610122"},{"code":610124,"name":"周至","fullname":"周至县","level":"district","filename":"610000/610100/610124"}]},{"code":610200,"name":"铜川","fullname":"铜川市","level":"city","filename":"610000/610200","children":[{"code":610202,"name":"王益","fullname":"王益区","level":"district","filename":"610000/610200/610202"},{"code":610203,"name":"印台","fullname":"印台区","level":"district","filename":"610000/610200/610203"},{"code":610204,"name":"耀州","fullname":"耀州区","level":"district","filename":"610000/610200/610204"},{"code":610222,"name":"宜君","fullname":"宜君县","level":"district","filename":"610000/610200/610222"}]},{"code":610300,"name":"宝鸡","fullname":"宝鸡市","level":"city","filename":"610000/610300","children":[{"code":610302,"name":"渭滨","fullname":"渭滨区","level":"district","filename":"610000/610300/610302"},{"code":610303,"name":"金台","fullname":"金台区","level":"district","filename":"610000/610300/610303"},{"code":610304,"name":"陈仓","fullname":"陈仓区","level":"district","filename":"610000/610300/610304"},{"code":610322,"name":"凤翔","fullname":"凤翔区","level":"district","filename":"610000/610300/610322"},{"code":610323,"name":"岐山","fullname":"岐山县","level":"district","filename":"610000/610300/610323"},{"code":610324,"name":"扶风","fullname":"扶风县","level":"district","filename":"610000/610300/610324"},{"code":610326,"name":"眉县","fullname":"眉县","level":"district","filename":"610000/610300/610326"},{"code":610327,"name":"陇县","fullname":"陇县","level":"district","filename":"610000/610300/610327"},{"code":610328,"name":"千阳","fullname":"千阳县","level":"district","filename":"610000/610300/610328"},{"code":610329,"name":"麟游","fullname":"麟游县","level":"district","filename":"610000/610300/610329"},{"code":610330,"name":"凤县","fullname":"凤县","level":"district","filename":"610000/610300/610330"},{"code":610331,"name":"太白","fullname":"太白县","level":"district","filename":"610000/610300/610331"}]},{"code":610400,"name":"咸阳","fullname":"咸阳市","level":"city","filename":"610000/610400","children":[{"code":610402,"name":"秦都","fullname":"秦都区","level":"district","filename":"610000/610400/610402"},{"code":610403,"name":"杨陵","fullname":"杨陵区","level":"district","filename":"610000/610400/610403"},{"code":610404,"name":"渭城","fullname":"渭城区","level":"district","filename":"610000/610400/610404"},{"code":610422,"name":"三原","fullname":"三原县","level":"district","filename":"610000/610400/610422"},{"code":610423,"name":"泾阳","fullname":"泾阳县","level":"district","filename":"610000/610400/610423"},{"code":610424,"name":"乾县","fullname":"乾县","level":"district","filename":"610000/610400/610424"},{"code":610425,"name":"礼泉","fullname":"礼泉县","level":"district","filename":"610000/610400/610425"},{"code":610426,"name":"永寿","fullname":"永寿县","level":"district","filename":"610000/610400/610426"},{"code":610428,"name":"长武","fullname":"长武县","level":"district","filename":"610000/610400/610428"},{"code":610429,"name":"旬邑","fullname":"旬邑县","level":"district","filename":"610000/610400/610429"},{"code":610430,"name":"淳化","fullname":"淳化县","level":"district","filename":"610000/610400/610430"},{"code":610431,"name":"武功","fullname":"武功县","level":"district","filename":"610000/610400/610431"},{"code":610481,"name":"兴平","fullname":"兴平市","level":"district","filename":"610000/610400/610481"},{"code":610482,"name":"彬州","fullname":"彬州市","level":"district","filename":"610000/610400/610482"}]},{"code":610500,"name":"渭南","fullname":"渭南市","level":"city","filename":"610000/610500","children":[{"code":610502,"name":"临渭","fullname":"临渭区","level":"district","filename":"610000/610500/610502"},{"code":610503,"name":"华州","fullname":"华州区","level":"district","filename":"610000/610500/610503"},{"code":610522,"name":"潼关","fullname":"潼关县","level":"district","filename":"610000/610500/610522"},{"code":610523,"name":"大荔","fullname":"大荔县","level":"district","filename":"610000/610500/610523"},{"code":610524,"name":"合阳","fullname":"合阳县","level":"district","filename":"610000/610500/610524"},{"code":610525,"name":"澄城","fullname":"澄城县","level":"district","filename":"610000/610500/610525"},{"code":610526,"name":"蒲城","fullname":"蒲城县","level":"district","filename":"610000/610500/610526"},{"code":610527,"name":"白水","fullname":"白水县","level":"district","filename":"610000/610500/610527"},{"code":610528,"name":"富平","fullname":"富平县","level":"district","filename":"610000/610500/610528"},{"code":610581,"name":"韩城","fullname":"韩城市","level":"district","filename":"610000/610500/610581"},{"code":610582,"name":"华阴","fullname":"华阴市","level":"district","filename":"610000/610500/610582"}]},{"code":610600,"name":"延安","fullname":"延安市","level":"city","filename":"610000/610600","children":[{"code":610602,"name":"宝塔","fullname":"宝塔区","level":"district","filename":"610000/610600/610602"},{"code":610603,"name":"安塞","fullname":"安塞区","level":"district","filename":"610000/610600/610603"},{"code":610621,"name":"延长","fullname":"延长县","level":"district","filename":"610000/610600/610621"},{"code":610622,"name":"延川","fullname":"延川县","level":"district","filename":"610000/610600/610622"},{"code":610625,"name":"志丹","fullname":"志丹县","level":"district","filename":"610000/610600/610625"},{"code":610626,"name":"吴起","fullname":"吴起县","level":"district","filename":"610000/610600/610626"},{"code":610627,"name":"甘泉","fullname":"甘泉县","level":"district","filename":"610000/610600/610627"},{"code":610628,"name":"富县","fullname":"富县","level":"district","filename":"610000/610600/610628"},{"code":610629,"name":"洛川","fullname":"洛川县","level":"district","filename":"610000/610600/610629"},{"code":610630,"name":"宜川","fullname":"宜川县","level":"district","filename":"610000/610600/610630"},{"code":610631,"name":"黄龙","fullname":"黄龙县","level":"district","filename":"610000/610600/610631"},{"code":610632,"name":"黄陵","fullname":"黄陵县","level":"district","filename":"610000/610600/610632"},{"code":610681,"name":"子长","fullname":"子长市","level":"district","filename":"610000/610600/610681"}]},{"code":610700,"name":"汉中","fullname":"汉中市","level":"city","filename":"610000/610700","children":[{"code":610702,"name":"汉台","fullname":"汉台区","level":"district","filename":"610000/610700/610702"},{"code":610703,"name":"南郑","fullname":"南郑区","level":"district","filename":"610000/610700/610703"},{"code":610722,"name":"城固","fullname":"城固县","level":"district","filename":"610000/610700/610722"},{"code":610723,"name":"洋县","fullname":"洋县","level":"district","filename":"610000/610700/610723"},{"code":610724,"name":"西乡","fullname":"西乡县","level":"district","filename":"610000/610700/610724"},{"code":610725,"name":"勉县","fullname":"勉县","level":"district","filename":"610000/610700/610725"},{"code":610726,"name":"宁强","fullname":"宁强县","level":"district","filename":"610000/610700/610726"},{"code":610727,"name":"略阳","fullname":"略阳县","level":"district","filename":"610000/610700/610727"},{"code":610728,"name":"镇巴","fullname":"镇巴县","level":"district","filename":"610000/610700/610728"},{"code":610729,"name":"留坝","fullname":"留坝县","level":"district","filename":"610000/610700/610729"},{"code":610730,"name":"佛坪","fullname":"佛坪县","level":"district","filename":"610000/610700/610730"}]},{"code":610800,"name":"榆林","fullname":"榆林市","level":"city","filename":"610000/610800","children":[{"code":610802,"name":"榆阳","fullname":"榆阳区","level":"district","filename":"610000/610800/610802"},{"code":610803,"name":"横山","fullname":"横山区","level":"district","filename":"610000/610800/610803"},{"code":610822,"name":"府谷","fullname":"府谷县","level":"district","filename":"610000/610800/610822"},{"code":610824,"name":"靖边","fullname":"靖边县","level":"district","filename":"610000/610800/610824"},{"code":610825,"name":"定边","fullname":"定边县","level":"district","filename":"610000/610800/610825"},{"code":610826,"name":"绥德","fullname":"绥德县","level":"district","filename":"610000/610800/610826"},{"code":610827,"name":"米脂","fullname":"米脂县","level":"district","filename":"610000/610800/610827"},{"code":610828,"name":"佳县","fullname":"佳县","level":"district","filename":"610000/610800/610828"},{"code":610829,"name":"吴堡","fullname":"吴堡县","level":"district","filename":"610000/610800/610829"},{"code":610830,"name":"清涧","fullname":"清涧县","level":"district","filename":"610000/610800/610830"},{"code":610831,"name":"子洲","fullname":"子洲县","level":"district","filename":"610000/610800/610831"},{"code":610881,"name":"神木","fullname":"神木市","level":"district","filename":"610000/610800/610881"}]},{"code":610900,"name":"安康","fullname":"安康市","level":"city","filename":"610000/610900","children":[{"code":610902,"name":"汉滨","fullname":"汉滨区","level":"district","filename":"610000/610900/610902"},{"code":610921,"name":"汉阴","fullname":"汉阴县","level":"district","filename":"610000/610900/610921"},{"code":610922,"name":"石泉","fullname":"石泉县","level":"district","filename":"610000/610900/610922"},{"code":610923,"name":"宁陕","fullname":"宁陕县","level":"district","filename":"610000/610900/610923"},{"code":610924,"name":"紫阳","fullname":"紫阳县","level":"district","filename":"610000/610900/610924"},{"code":610925,"name":"岚皋","fullname":"岚皋县","level":"district","filename":"610000/610900/610925"},{"code":610926,"name":"平利","fullname":"平利县","level":"district","filename":"610000/610900/610926"},{"code":610927,"name":"镇坪","fullname":"镇坪县","level":"district","filename":"610000/610900/610927"},{"code":610928,"name":"旬阳","fullname":"旬阳市","level":"district","filename":"610000/610900/610928"},{"code":610929,"name":"白河","fullname":"白河县","level":"district","filename":"610000/610900/610929"}]},{"code":611000,"name":"商洛","fullname":"商洛市","level":"city","filename":"610000/611000","children":[{"code":611002,"name":"商州","fullname":"商州区","level":"district","filename":"610000/611000/611002"},{"code":611021,"name":"洛南","fullname":"洛南县","level":"district","filename":"610000/611000/611021"},{"code":611022,"name":"丹凤","fullname":"丹凤县","level":"district","filename":"610000/611000/611022"},{"code":611023,"name":"商南","fullname":"商南县","level":"district","filename":"610000/611000/611023"},{"code":611024,"name":"山阳","fullname":"山阳县","level":"district","filename":"610000/611000/611024"},{"code":611025,"name":"镇安","fullname":"镇安县","level":"district","filename":"610000/611000/611025"},{"code":611026,"name":"柞水","fullname":"柞水县","level":"district","filename":"610000/611000/611026"}]}]},{"code":620000,"name":"甘肃","fullname":"甘肃省","level":"province","filename":"620000","children":[{"code":620100,"name":"兰州","fullname":"兰州市","level":"city","filename":"620000/620100","children":[{"code":620102,"name":"城关","fullname":"城关区","level":"district","filename":"620000/620100/620102"},{"code":620103,"name":"七里河","fullname":"七里河区","level":"district","filename":"620000/620100/620103"},{"code":620104,"name":"西固","fullname":"西固区","level":"district","filename":"620000/620100/620104"},{"code":620105,"name":"安宁","fullname":"安宁区","level":"district","filename":"620000/620100/620105"},{"code":620111,"name":"红古","fullname":"红古区","level":"district","filename":"620000/620100/620111"},{"code":620121,"name":"永登","fullname":"永登县","level":"district","filename":"620000/620100/620121"},{"code":620122,"name":"皋兰","fullname":"皋兰县","level":"district","filename":"620000/620100/620122"},{"code":620123,"name":"榆中","fullname":"榆中县","level":"district","filename":"620000/620100/620123"}]},{"code":620200,"name":"嘉峪关","fullname":"嘉峪关市","level":"city","filename":"620000/620200"},{"code":620300,"name":"金昌","fullname":"金昌市","level":"city","filename":"620000/620300","children":[{"code":620302,"name":"金川","fullname":"金川区","level":"district","filename":"620000/620300/620302"},{"code":620321,"name":"永昌","fullname":"永昌县","level":"district","filename":"620000/620300/620321"}]},{"code":620400,"name":"白银","fullname":"白银市","level":"city","filename":"620000/620400","children":[{"code":620402,"name":"白银","fullname":"白银区","level":"district","filename":"620000/620400/620402"},{"code":620403,"name":"平川","fullname":"平川区","level":"district","filename":"620000/620400/620403"},{"code":620421,"name":"靖远","fullname":"靖远县","level":"district","filename":"620000/620400/620421"},{"code":620422,"name":"会宁","fullname":"会宁县","level":"district","filename":"620000/620400/620422"},{"code":620423,"name":"景泰","fullname":"景泰县","level":"district","filename":"620000/620400/620423"}]},{"code":620500,"name":"天水","fullname":"天水市","level":"city","filename":"620000/620500","children":[{"code":620502,"name":"秦州","fullname":"秦州区","level":"district","filename":"620000/620500/620502"},{"code":620503,"name":"麦积","fullname":"麦积区","level":"district","filename":"620000/620500/620503"},{"code":620521,"name":"清水","fullname":"清水县","level":"district","filename":"620000/620500/620521"},{"code":620522,"name":"秦安","fullname":"秦安县","level":"district","filename":"620000/620500/620522"},{"code":620523,"name":"甘谷","fullname":"甘谷县","level":"district","filename":"620000/620500/620523"},{"code":620524,"name":"武山","fullname":"武山县","level":"district","filename":"620000/620500/620524"},{"code":620525,"name":"张家川","fullname":"张家川回族自治县","level":"district","filename":"620000/620500/620525"}]},{"code":620600,"name":"武威","fullname":"武威市","level":"city","filename":"620000/620600","children":[{"code":620602,"name":"凉州","fullname":"凉州区","level":"district","filename":"620000/620600/620602"},{"code":620621,"name":"民勤","fullname":"民勤县","level":"district","filename":"620000/620600/620621"},{"code":620622,"name":"古浪","fullname":"古浪县","level":"district","filename":"620000/620600/620622"},{"code":620623,"name":"天祝","fullname":"天祝藏族自治县","level":"district","filename":"620000/620600/620623"}]},{"code":620700,"name":"张掖","fullname":"张掖市","level":"city","filename":"620000/620700","children":[{"code":620702,"name":"甘州","fullname":"甘州区","level":"district","filename":"620000/620700/620702"},{"code":620721,"name":"肃南","fullname":"肃南裕固族自治县","level":"district","filename":"620000/620700/620721"},{"code":620722,"name":"民乐","fullname":"民乐县","level":"district","filename":"620000/620700/620722"},{"code":620723,"name":"临泽","fullname":"临泽县","level":"district","filename":"620000/620700/620723"},{"code":620724,"name":"高台","fullname":"高台县","level":"district","filename":"620000/620700/620724"},{"code":620725,"name":"山丹","fullname":"山丹县","level":"district","filename":"620000/620700/620725"}]},{"code":620800,"name":"平凉","fullname":"平凉市","level":"city","filename":"620000/620800","children":[{"code":620802,"name":"崆峒","fullname":"崆峒区","level":"district","filename":"620000/620800/620802"},{"code":620821,"name":"泾川","fullname":"泾川县","level":"district","filename":"620000/620800/620821"},{"code":620822,"name":"灵台","fullname":"灵台县","level":"district","filename":"620000/620800/620822"},{"code":620823,"name":"崇信","fullname":"崇信县","level":"district","filename":"620000/620800/620823"},{"code":620825,"name":"庄浪","fullname":"庄浪县","level":"district","filename":"620000/620800/620825"},{"code":620826,"name":"静宁","fullname":"静宁县","level":"district","filename":"620000/620800/620826"},{"code":620881,"name":"华亭","fullname":"华亭市","level":"district","filename":"620000/620800/620881"}]},{"code":620900,"name":"酒泉","fullname":"酒泉市","level":"city","filename":"620000/620900","children":[{"code":620902,"name":"肃州","fullname":"肃州区","level":"district","filename":"620000/620900/620902"},{"code":620921,"name":"金塔","fullname":"金塔县","level":"district","filename":"620000/620900/620921"},{"code":620922,"name":"瓜州","fullname":"瓜州县","level":"district","filename":"620000/620900/620922"},{"code":620923,"name":"肃北","fullname":"肃北蒙古族自治县","level":"district","filename":"620000/620900/620923"},{"code":620924,"name":"阿克塞","fullname":"阿克塞哈萨克族自治县","level":"district","filename":"620000/620900/620924"},{"code":620981,"name":"玉门","fullname":"玉门市","level":"district","filename":"620000/620900/620981"},{"code":620982,"name":"敦煌","fullname":"敦煌市","level":"district","filename":"620000/620900/620982"}]},{"code":621000,"name":"庆阳","fullname":"庆阳市","level":"city","filename":"620000/621000","children":[{"code":621002,"name":"西峰","fullname":"西峰区","level":"district","filename":"620000/621000/621002"},{"code":621021,"name":"庆城","fullname":"庆城县","level":"district","filename":"620000/621000/621021"},{"code":621022,"name":"环县","fullname":"环县","level":"district","filename":"620000/621000/621022"},{"code":621023,"name":"华池","fullname":"华池县","level":"district","filename":"620000/621000/621023"},{"code":621024,"name":"合水","fullname":"合水县","level":"district","filename":"620000/621000/621024"},{"code":621025,"name":"正宁","fullname":"正宁县","level":"district","filename":"620000/621000/621025"},{"code":621026,"name":"宁县","fullname":"宁县","level":"district","filename":"620000/621000/621026"},{"code":621027,"name":"镇原","fullname":"镇原县","level":"district","filename":"620000/621000/621027"}]},{"code":621100,"name":"定西","fullname":"定西市","level":"city","filename":"620000/621100","children":[{"code":621102,"name":"安定","fullname":"安定区","level":"district","filename":"620000/621100/621102"},{"code":621121,"name":"通渭","fullname":"通渭县","level":"district","filename":"620000/621100/621121"},{"code":621122,"name":"陇西","fullname":"陇西县","level":"district","filename":"620000/621100/621122"},{"code":621123,"name":"渭源","fullname":"渭源县","level":"district","filename":"620000/621100/621123"},{"code":621124,"name":"临洮","fullname":"临洮县","level":"district","filename":"620000/621100/621124"},{"code":621125,"name":"漳县","fullname":"漳县","level":"district","filename":"620000/621100/621125"},{"code":621126,"name":"岷县","fullname":"岷县","level":"district","filename":"620000/621100/621126"}]},{"code":621200,"name":"陇南","fullname":"陇南市","level":"city","filename":"620000/621200","children":[{"code":621202,"name":"武都","fullname":"武都区","level":"district","filename":"620000/621200/621202"},{"code":621221,"name":"成县","fullname":"成县","level":"district","filename":"620000/621200/621221"},{"code":621222,"name":"文县","fullname":"文县","level":"district","filename":"620000/621200/621222"},{"code":621223,"name":"宕昌","fullname":"宕昌县","level":"district","filename":"620000/621200/621223"},{"code":621224,"name":"康县","fullname":"康县","level":"district","filename":"620000/621200/621224"},{"code":621225,"name":"西和","fullname":"西和县","level":"district","filename":"620000/621200/621225"},{"code":621226,"name":"礼县","fullname":"礼县","level":"district","filename":"620000/621200/621226"},{"code":621227,"name":"徽县","fullname":"徽县","level":"district","filename":"620000/621200/621227"},{"code":621228,"name":"两当","fullname":"两当县","level":"district","filename":"620000/621200/621228"}]},{"code":622900,"name":"临夏","fullname":"临夏回族自治州","level":"city","filename":"620000/622900","children":[{"code":622901,"name":"临夏","fullname":"临夏市","level":"district","filename":"620000/622900/622901"},{"code":622921,"name":"临夏","fullname":"临夏县","level":"district","filename":"620000/622900/622921"},{"code":622922,"name":"康乐","fullname":"康乐县","level":"district","filename":"620000/622900/622922"},{"code":622923,"name":"永靖","fullname":"永靖县","level":"district","filename":"620000/622900/622923"},{"code":622924,"name":"广河","fullname":"广河县","level":"district","filename":"620000/622900/622924"},{"code":622925,"name":"和政","fullname":"和政县","level":"district","filename":"620000/622900/622925"},{"code":622926,"name":"东乡族","fullname":"东乡族自治县","level":"district","filename":"620000/622900/622926"},{"code":622927,"name":"积石山","fullname":"积石山保安族东乡族撒拉族自治县","level":"district","filename":"620000/622900/622927"}]},{"code":623000,"name":"甘南","fullname":"甘南藏族自治州","level":"city","filename":"620000/623000","children":[{"code":623001,"name":"合作","fullname":"合作市","level":"district","filename":"620000/623000/623001"},{"code":623021,"name":"临潭","fullname":"临潭县","level":"district","filename":"620000/623000/623021"},{"code":623022,"name":"卓尼","fullname":"卓尼县","level":"district","filename":"620000/623000/623022"},{"code":623023,"name":"舟曲","fullname":"舟曲县","level":"district","filename":"620000/623000/623023"},{"code":623024,"name":"迭部","fullname":"迭部县","level":"district","filename":"620000/623000/623024"},{"code":623025,"name":"玛曲","fullname":"玛曲县","level":"district","filename":"620000/623000/623025"},{"code":623026,"name":"碌曲","fullname":"碌曲县","level":"district","filename":"620000/623000/623026"},{"code":623027,"name":"夏河","fullname":"夏河县","level":"district","filename":"620000/623000/623027"}]}]},{"code":630000,"name":"青海","fullname":"青海省","level":"province","filename":"630000","children":[{"code":630100,"name":"西宁","fullname":"西宁市","level":"city","filename":"630000/630100","children":[{"code":630102,"name":"城东","fullname":"城东区","level":"district","filename":"630000/630100/630102"},{"code":630103,"name":"城中","fullname":"城中区","level":"district","filename":"630000/630100/630103"},{"code":630104,"name":"城西","fullname":"城西区","level":"district","filename":"630000/630100/630104"},{"code":630105,"name":"城北","fullname":"城北区","level":"district","filename":"630000/630100/630105"},{"code":630106,"name":"湟中","fullname":"湟中区","level":"district","filename":"630000/630100/630106"},{"code":630121,"name":"大通","fullname":"大通回族土族自治县","level":"district","filename":"630000/630100/630121"},{"code":630123,"name":"湟源","fullname":"湟源县","level":"district","filename":"630000/630100/630123"}]},{"code":630200,"name":"海东","fullname":"海东市","level":"city","filename":"630000/630200","children":[{"code":630202,"name":"乐都","fullname":"乐都区","level":"district","filename":"630000/630200/630202"},{"code":630203,"name":"平安","fullname":"平安区","level":"district","filename":"630000/630200/630203"},{"code":630222,"name":"民和","fullname":"民和回族土族自治县","level":"district","filename":"630000/630200/630222"},{"code":630223,"name":"互助","fullname":"互助土族自治县","level":"district","filename":"630000/630200/630223"},{"code":630224,"name":"化隆","fullname":"化隆回族自治县","level":"district","filename":"630000/630200/630224"},{"code":630225,"name":"循化","fullname":"循化撒拉族自治县","level":"district","filename":"630000/630200/630225"}]},{"code":632200,"name":"海北","fullname":"海北藏族自治州","level":"city","filename":"630000/632200","children":[{"code":632221,"name":"门源","fullname":"门源回族自治县","level":"district","filename":"630000/632200/632221"},{"code":632222,"name":"祁连","fullname":"祁连县","level":"district","filename":"630000/632200/632222"},{"code":632223,"name":"海晏","fullname":"海晏县","level":"district","filename":"630000/632200/632223"},{"code":632224,"name":"刚察","fullname":"刚察县","level":"district","filename":"630000/632200/632224"}]},{"code":632300,"name":"黄南","fullname":"黄南藏族自治州","level":"city","filename":"630000/632300","children":[{"code":632301,"name":"同仁","fullname":"同仁市","level":"district","filename":"630000/632300/632301"},{"code":632322,"name":"尖扎","fullname":"尖扎县","level":"district","filename":"630000/632300/632322"},{"code":632323,"name":"泽库","fullname":"泽库县","level":"district","filename":"630000/632300/632323"},{"code":632324,"name":"河南","fullname":"河南蒙古族自治县","level":"district","filename":"630000/632300/632324"}]},{"code":632500,"name":"海南","fullname":"海南藏族自治州","level":"city","filename":"630000/632500","children":[{"code":632521,"name":"共和","fullname":"共和县","level":"district","filename":"630000/632500/632521"},{"code":632522,"name":"同德","fullname":"同德县","level":"district","filename":"630000/632500/632522"},{"code":632523,"name":"贵德","fullname":"贵德县","level":"district","filename":"630000/632500/632523"},{"code":632524,"name":"兴海","fullname":"兴海县","level":"district","filename":"630000/632500/632524"},{"code":632525,"name":"贵南","fullname":"贵南县","level":"district","filename":"630000/632500/632525"}]},{"code":632600,"name":"果洛","fullname":"果洛藏族自治州","level":"city","filename":"630000/632600","children":[{"code":632621,"name":"玛沁","fullname":"玛沁县","level":"district","filename":"630000/632600/632621"},{"code":632622,"name":"班玛","fullname":"班玛县","level":"district","filename":"630000/632600/632622"},{"code":632623,"name":"甘德","fullname":"甘德县","level":"district","filename":"630000/632600/632623"},{"code":632624,"name":"达日","fullname":"达日县","level":"district","filename":"630000/632600/632624"},{"code":632625,"name":"久治","fullname":"久治县","level":"district","filename":"630000/632600/632625"},{"code":632626,"name":"玛多","fullname":"玛多县","level":"district","filename":"630000/632600/632626"}]},{"code":632700,"name":"玉树","fullname":"玉树藏族自治州","level":"city","filename":"630000/632700","children":[{"code":632701,"name":"玉树","fullname":"玉树市","level":"district","filename":"630000/632700/632701"},{"code":632722,"name":"杂多","fullname":"杂多县","level":"district","filename":"630000/632700/632722"},{"code":632723,"name":"称多","fullname":"称多县","level":"district","filename":"630000/632700/632723"},{"code":632724,"name":"治多","fullname":"治多县","level":"district","filename":"630000/632700/632724"},{"code":632725,"name":"囊谦","fullname":"囊谦县","level":"district","filename":"630000/632700/632725"},{"code":632726,"name":"曲麻莱","fullname":"曲麻莱县","level":"district","filename":"630000/632700/632726"}]},{"code":632800,"name":"海西","fullname":"海西蒙古族藏族自治州","level":"city","filename":"630000/632800","children":[{"code":632801,"name":"格尔木","fullname":"格尔木市","level":"district","filename":"630000/632800/632801"},{"code":632802,"name":"德令哈","fullname":"德令哈市","level":"district","filename":"630000/632800/632802"},{"code":632803,"name":"茫崖","fullname":"茫崖市","level":"district","filename":"630000/632800/632803"},{"code":632821,"name":"乌兰","fullname":"乌兰县","level":"district","filename":"630000/632800/632821"},{"code":632822,"name":"都兰","fullname":"都兰县","level":"district","filename":"630000/632800/632822"},{"code":632823,"name":"天峻","fullname":"天峻县","level":"district","filename":"630000/632800/632823"},{"code":632825,"name":"自治州直辖","fullname":"海西蒙古族藏族自治州直辖","level":"district","filename":"630000/632800/632825"}]}]},{"code":640000,"name":"宁夏","fullname":"宁夏回族自治区","level":"province","filename":"640000","children":[{"code":640100,"name":"银川","fullname":"银川市","level":"city","filename":"640000/640100","children":[{"code":640104,"name":"兴庆","fullname":"兴庆区","level":"district","filename":"640000/640100/640104"},{"code":640105,"name":"西夏","fullname":"西夏区","level":"district","filename":"640000/640100/640105"},{"code":640106,"name":"金凤","fullname":"金凤区","level":"district","filename":"640000/640100/640106"},{"code":640121,"name":"永宁","fullname":"永宁县","level":"district","filename":"640000/640100/640121"},{"code":640122,"name":"贺兰","fullname":"贺兰县","level":"district","filename":"640000/640100/640122"},{"code":640181,"name":"灵武","fullname":"灵武市","level":"district","filename":"640000/640100/640181"}]},{"code":640200,"name":"石嘴山","fullname":"石嘴山市","level":"city","filename":"640000/640200","children":[{"code":640202,"name":"大武口","fullname":"大武口区","level":"district","filename":"640000/640200/640202"},{"code":640205,"name":"惠农","fullname":"惠农区","level":"district","filename":"640000/640200/640205"},{"code":640221,"name":"平罗","fullname":"平罗县","level":"district","filename":"640000/640200/640221"}]},{"code":640300,"name":"吴忠","fullname":"吴忠市","level":"city","filename":"640000/640300","children":[{"code":640302,"name":"利通","fullname":"利通区","level":"district","filename":"640000/640300/640302"},{"code":640303,"name":"红寺堡","fullname":"红寺堡区","level":"district","filename":"640000/640300/640303"},{"code":640323,"name":"盐池","fullname":"盐池县","level":"district","filename":"640000/640300/640323"},{"code":640324,"name":"同心","fullname":"同心县","level":"district","filename":"640000/640300/640324"},{"code":640381,"name":"青铜峡","fullname":"青铜峡市","level":"district","filename":"640000/640300/640381"}]},{"code":640400,"name":"固原","fullname":"固原市","level":"city","filename":"640000/640400","children":[{"code":640402,"name":"原州","fullname":"原州区","level":"district","filename":"640000/640400/640402"},{"code":640422,"name":"西吉","fullname":"西吉县","level":"district","filename":"640000/640400/640422"},{"code":640423,"name":"隆德","fullname":"隆德县","level":"district","filename":"640000/640400/640423"},{"code":640424,"name":"泾源","fullname":"泾源县","level":"district","filename":"640000/640400/640424"},{"code":640425,"name":"彭阳","fullname":"彭阳县","level":"district","filename":"640000/640400/640425"}]},{"code":640500,"name":"中卫","fullname":"中卫市","level":"city","filename":"640000/640500","children":[{"code":640502,"name":"沙坡头","fullname":"沙坡头区","level":"district","filename":"640000/640500/640502"},{"code":640521,"name":"中宁","fullname":"中宁县","level":"district","filename":"640000/640500/640521"},{"code":640522,"name":"海原","fullname":"海原县","level":"district","filename":"640000/640500/640522"}]}]},{"code":650000,"name":"新疆","fullname":"新疆维吾尔自治区","level":"province","filename":"650000","children":[{"code":650100,"name":"乌鲁木齐","fullname":"乌鲁木齐市","level":"city","filename":"650000/650100","children":[{"code":650102,"name":"天山","fullname":"天山区","level":"district","filename":"650000/650100/650102"},{"code":650103,"name":"沙依巴克","fullname":"沙依巴克区","level":"district","filename":"650000/650100/650103"},{"code":650104,"name":"新市区","fullname":"新市区","level":"district","filename":"650000/650100/650104"},{"code":650105,"name":"水磨沟","fullname":"水磨沟区","level":"district","filename":"650000/650100/650105"},{"code":650106,"name":"头屯河","fullname":"头屯河区","level":"district","filename":"650000/650100/650106"},{"code":650107,"name":"达坂城","fullname":"达坂城区","level":"district","filename":"650000/650100/650107"},{"code":650109,"name":"米东","fullname":"米东区","level":"district","filename":"650000/650100/650109"},{"code":650121,"name":"乌鲁木齐","fullname":"乌鲁木齐县","level":"district","filename":"650000/650100/650121"}]},{"code":650200,"name":"克拉玛依","fullname":"克拉玛依市","level":"city","filename":"650000/650200","children":[{"code":650202,"name":"独山子","fullname":"独山子区","level":"district","filename":"650000/650200/650202"},{"code":650203,"name":"克拉玛依","fullname":"克拉玛依区","level":"district","filename":"650000/650200/650203"},{"code":650204,"name":"白碱滩","fullname":"白碱滩区","level":"district","filename":"650000/650200/650204"},{"code":650205,"name":"乌尔禾","fullname":"乌尔禾区","level":"district","filename":"650000/650200/650205"}]},{"code":650400,"name":"吐鲁番","fullname":"吐鲁番市","level":"city","filename":"650000/650400","children":[{"code":650402,"name":"高昌","fullname":"高昌区","level":"district","filename":"650000/650400/650402"},{"code":650421,"name":"鄯善","fullname":"鄯善县","level":"district","filename":"650000/650400/650421"},{"code":650422,"name":"托克逊","fullname":"托克逊县","level":"district","filename":"650000/650400/650422"}]},{"code":650500,"name":"哈密","fullname":"哈密市","level":"city","filename":"650000/650500","children":[{"code":650502,"name":"伊州","fullname":"伊州区","level":"district","filename":"650000/650500/650502"},{"code":650521,"name":"巴里坤","fullname":"巴里坤哈萨克自治县","level":"district","filename":"650000/650500/650521"},{"code":650522,"name":"伊吾","fullname":"伊吾县","level":"district","filename":"650000/650500/650522"}]},{"code":652300,"name":"昌吉","fullname":"昌吉回族自治州","level":"city","filename":"650000/652300","children":[{"code":652301,"name":"昌吉","fullname":"昌吉市","level":"district","filename":"650000/652300/652301"},{"code":652302,"name":"阜康","fullname":"阜康市","level":"district","filename":"650000/652300/652302"},{"code":652323,"name":"呼图壁","fullname":"呼图壁县","level":"district","filename":"650000/652300/652323"},{"code":652324,"name":"玛纳斯","fullname":"玛纳斯县","level":"district","filename":"650000/652300/652324"},{"code":652325,"name":"奇台","fullname":"奇台县","level":"district","filename":"650000/652300/652325"},{"code":652327,"name":"吉木萨尔","fullname":"吉木萨尔县","level":"district","filename":"650000/652300/652327"},{"code":652328,"name":"木垒","fullname":"木垒哈萨克自治县","level":"district","filename":"650000/652300/652328"}]},{"code":652700,"name":"博尔塔拉","fullname":"博尔塔拉蒙古自治州","level":"city","filename":"650000/652700","children":[{"code":652701,"name":"博乐","fullname":"博乐市","level":"district","filename":"650000/652700/652701"},{"code":652702,"name":"阿拉山口","fullname":"阿拉山口市","level":"district","filename":"650000/652700/652702"},{"code":652722,"name":"精河","fullname":"精河县","level":"district","filename":"650000/652700/652722"},{"code":652723,"name":"温泉","fullname":"温泉县","level":"district","filename":"650000/652700/652723"}]},{"code":652800,"name":"巴音郭楞","fullname":"巴音郭楞蒙古自治州","level":"city","filename":"650000/652800","children":[{"code":652801,"name":"库尔勒","fullname":"库尔勒市","level":"district","filename":"650000/652800/652801"},{"code":652822,"name":"轮台","fullname":"轮台县","level":"district","filename":"650000/652800/652822"},{"code":652823,"name":"尉犁","fullname":"尉犁县","level":"district","filename":"650000/652800/652823"},{"code":652824,"name":"若羌","fullname":"若羌县","level":"district","filename":"650000/652800/652824"},{"code":652825,"name":"且末","fullname":"且末县","level":"district","filename":"650000/652800/652825"},{"code":652826,"name":"焉耆","fullname":"焉耆回族自治县","level":"district","filename":"650000/652800/652826"},{"code":652827,"name":"和静","fullname":"和静县","level":"district","filename":"650000/652800/652827"},{"code":652828,"name":"和硕","fullname":"和硕县","level":"district","filename":"650000/652800/652828"},{"code":652829,"name":"博湖","fullname":"博湖县","level":"district","filename":"650000/652800/652829"}]},{"code":652900,"name":"阿克苏","fullname":"阿克苏地区","level":"city","filename":"650000/652900","children":[{"code":652901,"name":"阿克苏","fullname":"阿克苏市","level":"district","filename":"650000/652900/652901"},{"code":652902,"name":"库车","fullname":"库车市","level":"district","filename":"650000/652900/652902"},{"code":652922,"name":"温宿","fullname":"温宿县","level":"district","filename":"650000/652900/652922"},{"code":652924,"name":"沙雅","fullname":"沙雅县","level":"district","filename":"650000/652900/652924"},{"code":652925,"name":"新和","fullname":"新和县","level":"district","filename":"650000/652900/652925"},{"code":652926,"name":"拜城","fullname":"拜城县","level":"district","filename":"650000/652900/652926"},{"code":652927,"name":"乌什","fullname":"乌什县","level":"district","filename":"650000/652900/652927"},{"code":652928,"name":"阿瓦提","fullname":"阿瓦提县","level":"district","filename":"650000/652900/652928"},{"code":652929,"name":"柯坪","fullname":"柯坪县","level":"district","filename":"650000/652900/652929"}]},{"code":653000,"name":"克孜勒苏","fullname":"克孜勒苏柯尔克孜自治州","level":"city","filename":"650000/653000","children":[{"code":653001,"name":"阿图什","fullname":"阿图什市","level":"district","filename":"650000/653000/653001"},{"code":653022,"name":"阿克陶","fullname":"阿克陶县","level":"district","filename":"650000/653000/653022"},{"code":653023,"name":"阿合奇","fullname":"阿合奇县","level":"district","filename":"650000/653000/653023"},{"code":653024,"name":"乌恰","fullname":"乌恰县","level":"district","filename":"650000/653000/653024"}]},{"code":653100,"name":"喀什","fullname":"喀什地区","level":"city","filename":"650000/653100","children":[{"code":653101,"name":"喀什","fullname":"喀什市","level":"district","filename":"650000/653100/653101"},{"code":653121,"name":"疏附","fullname":"疏附县","level":"district","filename":"650000/653100/653121"},{"code":653122,"name":"疏勒","fullname":"疏勒县","level":"district","filename":"650000/653100/653122"},{"code":653123,"name":"英吉沙","fullname":"英吉沙县","level":"district","filename":"650000/653100/653123"},{"code":653124,"name":"泽普","fullname":"泽普县","level":"district","filename":"650000/653100/653124"},{"code":653125,"name":"莎车","fullname":"莎车县","level":"district","filename":"650000/653100/653125"},{"code":653126,"name":"叶城","fullname":"叶城县","level":"district","filename":"650000/653100/653126"},{"code":653127,"name":"麦盖提","fullname":"麦盖提县","level":"district","filename":"650000/653100/653127"},{"code":653128,"name":"岳普湖","fullname":"岳普湖县","level":"district","filename":"650000/653100/653128"},{"code":653129,"name":"伽师","fullname":"伽师县","level":"district","filename":"650000/653100/653129"},{"code":653130,"name":"巴楚","fullname":"巴楚县","level":"district","filename":"650000/653100/653130"},{"code":653131,"name":"塔什库尔干","fullname":"塔什库尔干塔吉克自治县","level":"district","filename":"650000/653100/653131"}]},{"code":653200,"name":"和田","fullname":"和田地区","level":"city","filename":"650000/653200","children":[{"code":653201,"name":"和田","fullname":"和田市","level":"district","filename":"650000/653200/653201"},{"code":653221,"name":"和田","fullname":"和田县","level":"district","filename":"650000/653200/653221"},{"code":653222,"name":"墨玉","fullname":"墨玉县","level":"district","filename":"650000/653200/653222"},{"code":653223,"name":"皮山","fullname":"皮山县","level":"district","filename":"650000/653200/653223"},{"code":653224,"name":"洛浦","fullname":"洛浦县","level":"district","filename":"650000/653200/653224"},{"code":653225,"name":"策勒","fullname":"策勒县","level":"district","filename":"650000/653200/653225"},{"code":653226,"name":"于田","fullname":"于田县","level":"district","filename":"650000/653200/653226"},{"code":653227,"name":"民丰","fullname":"民丰县","level":"district","filename":"650000/653200/653227"}]},{"code":654000,"name":"伊犁","fullname":"伊犁哈萨克自治州","level":"city","filename":"650000/654000","children":[{"code":654002,"name":"伊宁","fullname":"伊宁市","level":"district","filename":"650000/654000/654002"},{"code":654003,"name":"奎屯","fullname":"奎屯市","level":"district","filename":"650000/654000/654003"},{"code":654004,"name":"霍尔果斯","fullname":"霍尔果斯市","level":"district","filename":"650000/654000/654004"},{"code":654021,"name":"伊宁","fullname":"伊宁县","level":"district","filename":"650000/654000/654021"},{"code":654022,"name":"察布查尔","fullname":"察布查尔锡伯自治县","level":"district","filename":"650000/654000/654022"},{"code":654023,"name":"霍城","fullname":"霍城县","level":"district","filename":"650000/654000/654023"},{"code":654024,"name":"巩留","fullname":"巩留县","level":"district","filename":"650000/654000/654024"},{"code":654025,"name":"新源","fullname":"新源县","level":"district","filename":"650000/654000/654025"},{"code":654026,"name":"昭苏","fullname":"昭苏县","level":"district","filename":"650000/654000/654026"},{"code":654027,"name":"特克斯","fullname":"特克斯县","level":"district","filename":"650000/654000/654027"},{"code":654028,"name":"尼勒克","fullname":"尼勒克县","level":"district","filename":"650000/654000/654028"}]},{"code":654200,"name":"塔城","fullname":"塔城地区","level":"city","filename":"650000/654200","children":[{"code":654201,"name":"塔城","fullname":"塔城市","level":"district","filename":"650000/654200/654201"},{"code":654202,"name":"乌苏","fullname":"乌苏市","level":"district","filename":"650000/654200/654202"},{"code":654221,"name":"额敏","fullname":"额敏县","level":"district","filename":"650000/654200/654221"},{"code":654223,"name":"沙湾","fullname":"沙湾市","level":"district","filename":"650000/654200/654223"},{"code":654224,"name":"托里","fullname":"托里县","level":"district","filename":"650000/654200/654224"},{"code":654225,"name":"裕民","fullname":"裕民县","level":"district","filename":"650000/654200/654225"},{"code":654226,"name":"和布克赛尔","fullname":"和布克赛尔蒙古自治县","level":"district","filename":"650000/654200/654226"}]},{"code":654300,"name":"阿勒泰","fullname":"阿勒泰地区","level":"city","filename":"650000/654300","children":[{"code":654301,"name":"阿勒泰","fullname":"阿勒泰市","level":"district","filename":"650000/654300/654301"},{"code":654321,"name":"布尔津","fullname":"布尔津县","level":"district","filename":"650000/654300/654321"},{"code":654322,"name":"富蕴","fullname":"富蕴县","level":"district","filename":"650000/654300/654322"},{"code":654323,"name":"福海","fullname":"福海县","level":"district","filename":"650000/654300/654323"},{"code":654324,"name":"哈巴河","fullname":"哈巴河县","level":"district","filename":"650000/654300/654324"},{"code":654325,"name":"青河","fullname":"青河县","level":"district","filename":"650000/654300/654325"},{"code":654326,"name":"吉木乃","fullname":"吉木乃县","level":"district","filename":"650000/654300/654326"}]},{"code":659001,"name":"石河子","fullname":"石河子市","level":"city","filename":"650000/659001"},{"code":659002,"name":"阿拉尔","fullname":"阿拉尔市","level":"city","filename":"650000/659002"},{"code":659003,"name":"图木舒克","fullname":"图木舒克市","level":"city","filename":"650000/659003"},{"code":659004,"name":"五家渠","fullname":"五家渠市","level":"city","filename":"650000/659004"},{"code":659005,"name":"北屯","fullname":"北屯市","level":"city","filename":"650000/659005"},{"code":659006,"name":"铁门关","fullname":"铁门关市","level":"city","filename":"650000/659006"},{"code":659007,"name":"双河","fullname":"双河市","level":"city","filename":"650000/659007"},{"code":659008,"name":"可克达拉","fullname":"可克达拉市","level":"city","filename":"650000/659008"},{"code":659009,"name":"昆玉","fullname":"昆玉市","level":"city","filename":"650000/659009"},{"code":659010,"name":"胡杨河","fullname":"胡杨河市","level":"city","filename":"650000/659010"}]},{"code":710000,"name":"台湾","fullname":"台湾省","level":"province","filename":"710000"},{"code":810000,"name":"香港","fullname":"香港特别行政区","level":"province","filename":"810000","children":[{"code":810001,"name":"中西","fullname":"中西区","level":"district","filename":"810000/810001"},{"code":810002,"name":"湾仔","fullname":"湾仔区","level":"district","filename":"810000/810002"},{"code":810003,"name":"东区","fullname":"东区","level":"district","filename":"810000/810003"},{"code":810004,"name":"南区","fullname":"南区","level":"district","filename":"810000/810004"},{"code":810005,"name":"油尖旺","fullname":"油尖旺区","level":"district","filename":"810000/810005"},{"code":810006,"name":"深水埗","fullname":"深水埗区","level":"district","filename":"810000/810006"},{"code":810007,"name":"九龙城","fullname":"九龙城区","level":"district","filename":"810000/810007"},{"code":810008,"name":"黄大仙","fullname":"黄大仙区","level":"district","filename":"810000/810008"},{"code":810009,"name":"观塘","fullname":"观塘区","level":"district","filename":"810000/810009"},{"code":810010,"name":"荃湾","fullname":"荃湾区","level":"district","filename":"810000/810010"},{"code":810011,"name":"屯门","fullname":"屯门区","level":"district","filename":"810000/810011"},{"code":810012,"name":"元朗","fullname":"元朗区","level":"district","filename":"810000/810012"},{"code":810013,"name":"北区","fullname":"北区","level":"district","filename":"810000/810013"},{"code":810014,"name":"大埔","fullname":"大埔区","level":"district","filename":"810000/810014"},{"code":810015,"name":"西贡","fullname":"西贡区","level":"district","filename":"810000/810015"},{"code":810016,"name":"沙田","fullname":"沙田区","level":"district","filename":"810000/810016"},{"code":810017,"name":"葵青","fullname":"葵青区","level":"district","filename":"810000/810017"},{"code":810018,"name":"离岛","fullname":"离岛区","level":"district","filename":"810000/810018"}]},{"code":820000,"name":"澳门","fullname":"澳门特别行政区","level":"province","filename":"820000","children":[{"code":820001,"name":"花地玛","fullname":"花地玛堂区","level":"district","filename":"820000/820001"},{"code":820002,"name":"花王","fullname":"花王堂区","level":"district","filename":"820000/820002"},{"code":820003,"name":"望德","fullname":"望德堂区","level":"district","filename":"820000/820003"},{"code":820004,"name":"大堂","fullname":"大堂区","level":"district","filename":"820000/820004"},{"code":820005,"name":"风顺","fullname":"风顺堂区","level":"district","filename":"820000/820005"},{"code":820006,"name":"嘉模","fullname":"嘉模堂区","level":"district","filename":"820000/820006"},{"code":820007,"name":"路凼","fullname":"路凼填海区","level":"district","filename":"820000/820007"},{"code":820008,"name":"圣方济各","fullname":"圣方济各堂区","level":"district","filename":"820000/820008"}]}]} \ No newline at end of file diff --git a/mes-webapi/src/main/resources/config/application-dev.yml b/mes-webapi/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..d5512ef --- /dev/null +++ b/mes-webapi/src/main/resources/config/application-dev.yml @@ -0,0 +1,350 @@ +--- ### 项目配置 +project: + # URL(跨域配置默认放行此 URL,第三方登录回调默认使用此 URL 为前缀,请注意更改为你实际的前端 URL) + url: http://localhost:6609 + +--- ### 服务器配置 +server: + # HTTP 端口(默认 6609) + port: 6609 + +--- ### 排除自动配置 +spring: + autoconfigure: + exclude: + - com.xkcoding.justauth.autoconfigure.JustAuthAutoConfiguration + +--- ### 数据源配置 +spring.datasource: + type: com.zaxxer.hikari.HikariDataSource + # 请务必提前创建好名为 wms_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置 + url: jdbc:p6spy:mysql://127.0.0.1:3306/wms_th?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + username: root + password: root +# password: test123$ + # PostgreSQL 配置 +# url: jdbc:p6spy:mysql://192.168.2.30:${DB_PORT:3306}/continew?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true +# username: ${DB_USER:root} +# password: ${DB_PWD:SQLsql123} +# url: jdbc:p6spy:mysql://81.68.71.142:${DB_PORT:3306}/continew?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true +# username: ${DB_USER:root} +# password: ${DB_PWD:MYsql12@} + driver-class-name: com.p6spy.engine.spy.P6SpyDriver + # Hikari 连接池配置 + hikari: + # 最大连接数量(默认 10,根据实际环境调整) + # 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒 + maximum-pool-size: 20 + # 获取连接超时时间(默认 30000 毫秒,30 秒) + connection-timeout: 30000 + # 空闲连接最大存活时间(默认 600000 毫秒,10 分钟) + idle-timeout: 600000 + # 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime(默认 0,禁用) + keepaliveTime: 30000 + # 连接最大生存时间(默认 1800000 毫秒,30 分钟) + max-lifetime: 1800000 +## Liquibase 配置 +spring.liquibase: + # 是否启用 + enabled: true + # 配置文件路径 + change-log: classpath:/db/changelog/db.changelog-master.yaml + +--- ### 缓存配置 +spring.data: + ## Redis 配置(单机模式) + redis: + # 地址 +# host: ${REDIS_HOST:192.168.2.30} + host: ${REDIS_HOST:127.0.0.1} + # 端口(默认 6379) + port: ${REDIS_PORT:6379} + # 密码(未设置密码时请注释掉) +# password: ${REDIS_PWD:redis2025} + # 数据库索引 + database: ${REDIS_DB:0} + # 连接超时时间 + timeout: 5s + # 是否开启 SSL + ssl: + enabled: false + ## Redisson 配置 + redisson: + enabled: true + mode: SINGLE +## JetCache 配置 +jetcache: + # 统计间隔(默认 0,表示不统计) + statIntervalMinutes: 15 + ## 本地/进程级/一级缓存配置 + local: + default: + # 缓存类型 + type: caffeine + # key 转换器的全局配置 + keyConvertor: jackson + # 以毫秒为单位指定超时时间的全局配置 + expireAfterWriteInMillis: 7200000 + # 每个缓存实例的最大元素的全局配置,仅 local 类型的缓存需要指定 + limit: 1000 + ## 远程/分布式/二级缓存配置 + remote: + default: + # 缓存类型 + type: redisson + # key 转换器的全局配置(用于将复杂的 KEY 类型转换为缓存实现可以接受的类型) + keyConvertor: jackson + # 以毫秒为单位指定超时时间的全局配置 + expireAfterWriteInMillis: 7200000 + # 2.7+ 支持两级缓存更新以后失效其他 JVM 中的 local cache,但多个服务共用 Redis 同一个 channel 可能会造成广播风暴,需要在这里指定channel。 + # 你可以决定多个不同的服务是否共用同一个 channel,如果没有指定则不开启。 + broadcastChannel: ${spring.application.name} + # 序列化器的全局配置,仅 remote 类型的缓存需要指定 + valueEncoder: java + valueDecoder: java + +--- ### 验证码配置 +continew-starter.captcha: + ## 行为验证码 + behavior: + enabled: false + ## 图形验证码 + graphic: + # 类型 + type: SPEC + # 内容长度 + length: 4 + # 过期时间 + expirationInMinutes: 2 +## 其他验证码配置 +captcha: + ## 邮箱验证码配置 + mail: + # 内容长度 + length: 6 + # 过期时间 + expirationInMinutes: 5 + # 模板路径 + templatePath: mail/captcha.ftl + ## 短信验证码配置 + sms: + # 内容长度 + length: 4 + # 过期时间 + expirationInMinutes: 5 + # 模板 ID + templateId: 1 + +--- ### 日志配置 +continew-starter.log: + # 是否打印日志,开启后可打印访问日志(类似于 Nginx access log) + is-print: true + # 忽略的路径,多个路径用逗号分隔 + exclude-patterns: ["/file/**", "/sdk/**"] +## 项目日志配置(配置重叠部分,优先级高于 logback-spring.xml 中的配置) +logging: + level: + top.continew.admin: WARN + top.continew.starter: WARN + file: + path: ./logs + +--- ### 跨域配置 +continew-starter.web.cors: + enabled: true + # 配置允许跨域的域名 + allowed-origins: '*' + # 配置允许跨域的请求方式 + allowed-methods: '*' + # 配置允许跨域的请求头 + allowed-headers: '*' + # 配置允许跨域的响应头 + exposed-headers: '*' + + +--- ### 接口文档配置 +springdoc: + swagger-ui: + enabled: true + +--- ### WebSocket 配置 +continew-starter.messaging.websocket: + enabled: true + path: /websocket + # 配置允许跨域的域名 + allowed-origins: '*' + +--- ### 短信配置(已禁用) +# sms: +# # 从 YAML 读取配置 +# config-type: YAML +# http-log: true +# is-print: false +# blends: +# cloopen: +# # 短信厂商 +# supplier: cloopen +# base-url: https://app.cloopen.com:8883/2013-12-26 +# access-key-id: 你的Access Key +# access-key-secret: 你的Access Key Secret +# sdk-app-id: 你的应用ID + +--- ### 邮件配置(已禁用) +# spring.mail: +# # 根据需要更换 +# host: smtp.126.com +# port: 465 +# username: 你的邮箱 +# password: 你的邮箱授权码 +# properties: +# mail: +# smtp: +# auth: true +# socketFactory: +# class: javax.net.ssl.SSLSocketFactory +# port: 465 + +--- ### Just Auth 配置 +justauth: + enabled: false + type: + GITEE: + client-id: 5d271b7f638941812aaf8bfc2e2f08f06d6235ef934e0e39537e2364eb8452c4 + client-secret: 1f7d08**********5b7**********29e + redirect-uri: ${project.url}/social/callback?source=gitee + GITHUB: + client-id: 38080dad08cfbdfacca9 + client-secret: 1f7d08**********5b7**********29e + redirect-uri: ${project.url}/social/callback?source=github + cache: + type: REDIS + +--- ### Sa-Token 扩展配置 +sa-token.extension: + # 安全配置:排除(放行)路径配置 + security.excludes: + - /error + # 静态资源 + - /*.html + - /*/*.html + - /*/*.css + - /*/*.js + - /websocket/** + # 接口文档相关资源 + - /favicon.ico + - /doc.html + - /webjars/** + - /swagger-ui/** + - /swagger-resources/** + - /*/api-docs/** + # 本地存储资源 + - /file/** + - /sdk/** + +--- ### 安全配置 +continew-starter.security: + ## 字段加/解密配置 + crypto: + enabled: true + # 对称加密算法密钥 + password: abcdefghijklmnop + # 非对称加密算法密钥(在线生成 RSA 密钥对:http://web.chacuo.net/netrsakeypair) + public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9uaUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ== + private-key: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV + ## 密码编码器配置 + password: + enabled: true + # BCryptPasswordEncoder + encoding-id: bcrypt + ## 限流器配置 + limiter: + enabled: true + key-prefix: RateLimiter + +--- ### 文件上传配置 +spring.servlet: + multipart: + enabled: true + # 单文件上传大小限制 + max-file-size: 5120MB + # 单次总上传文件大小限制 + max-request-size: 5120MB +## 头像配置 +avatar: + # 存储路径 + path: user/avatar/ + # 支持的后缀 + support-suffix: jpg,jpeg,png,gif + +--- ### Snail Job 配置 +snail-job: + enabled: false + # 客户端地址(默认自动获取本机 IP) + #host: 127.0.0.1 + # 客户端端口(默认:1789) + port: 1789 + # 命名空间 ID + namespace: ${SCHEDULE_NAMESPACE:764d604ec6fc45f68cd92514c40e9e1a} + # 分组名 + group: ${SCHEDULE_GROUP:wms-admin} + # 令牌 + token: ${SCHEDULE_TOKEN:SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj} + ## 服务端配置(任务调度中心) + server: + # 服务端地址,若服务端集群部署则此处配置域名 + host: ${SCHEDULE_HOST:127.0.0.1} + # Netty 端口号 + port: ${SCHEDULE_PORT:1788} + # API 配置 + api: + # URL + url: http://127.0.0.1:8001/snail-job + # 用户名 + username: ${SCHEDULE_USERNAME:admin} + # 密码 + password: ${SCHEDULE_PASSWORD:admin} + ## 重试数据批量上报滑动窗口配置 + retry: + reportSlidingWindow: + # 窗口期单位 + chrono-unit: SECONDS + # 窗口期时间长度 + duration: 10 + # 总量窗口期阈值 + total-threshold: 50 + # 窗口数量预警 + window-total-threshold: 150 + ## 调度线程池配置 + dispatcherThreadPool: + # 核心线程数 + corePoolSize: 16 + # 最大线程数 + maximumPoolSize: 16 + # 线程存活时间 + keepAliveTime: 1 + # 时间单位 + timeUnit: SECONDS + # 队列容量 + queueCapacity: 10000 + +# uni配置(已禁用) +# uni: +# url: http://wo-api.uni-ubi.com/ +# appKey: 7834BC94180542C6BFD7031B39266E6F +# appSecret: 97386E6DCEFC4758810CC720DC669662 +# projectGuid: 114C2FC2EA0B4A848C5F9AF23400E6F9 + +# Minio配置(已禁用,使用本地存储) +# minio: +# url: http://81.68.71.142:9000 +# accessKey: admin +# secretKey: JYadmin@1234 +# bucketName: employees +# bucketName1: visitor +# bucketName2: cars +# bucketName3: others +# gwurl: http://81.68.71.142:9000 + +# sdk配置(已禁用) +# sdk: +# upload: http://81.68.71.142:7701/ \ No newline at end of file diff --git a/mes-webapi/src/main/resources/config/application-generator.yml b/mes-webapi/src/main/resources/config/application-generator.yml new file mode 100644 index 0000000..e8e9530 --- /dev/null +++ b/mes-webapi/src/main/resources/config/application-generator.yml @@ -0,0 +1,141 @@ +--- ### 代码生成器配置 +generator: + # 排除数据表 + excludeTables: + - DATABASECHANGELOG + - DATABASECHANGELOGLOCK + - gen_config + - gen_field_config + ## 类型映射 + typeMappings: + MYSQL: + String: + - varchar + - char + - text + - mediumtext + - longtext + - tinytext + - json + Integer: + - int + - tinyint + - smallint + - mediumint + - integer + Long: + - bigint + Float: + - float + Double: + - double + Boolean: + - bit + BigDecimal: + - decimal + LocalDate: + - date + LocalDateTime: + - datetime + - timestamp + POSTGRE_SQL: + String: + - varchar + - char + - text + - json + Integer: + - int2 + - int4 + Long: + - int8 + Float: + - float4 + Double: + - float8 + Boolean: + - bool + BigDecimal: + - decimal + LocalDate: + - date + LocalDateTime: + - timestamp + ## 模板配置 + templateConfigs: + DO: + # 模板路径 + templatePath: backend/Entity.ftl + # 包名称 + packageName: model.entity + # 排除字段 + excludeFields: + - id + - createUser + - createTime + - updateUser + - updateTime + Query: + templatePath: backend/Query.ftl + packageName: model.query + Req: + templatePath: backend/Req.ftl + packageName: model.req + Resp: + templatePath: backend/Resp.ftl + packageName: model.resp + excludeFields: + - id + - createUser + - createTime + DetailResp: + templatePath: backend/DetailResp.ftl + packageName: model.resp + excludeFields: + - id + - createUser + - createTime + - updateUser + - updateTime + Mapper: + templatePath: backend/Mapper.ftl + packageName: mapper + MapperXml: + templatePath: backend/MapperXml.ftl + packageName: mapper + extension: .xml + suffix: Mapper + Service: + templatePath: backend/Service.ftl + packageName: service + ServiceImpl: + templatePath: backend/ServiceImpl.ftl + packageName: service.impl + Controller: + templatePath: backend/Controller.ftl + packageName: controller + api: + templatePath: frontend/api.ftl + packageName: src/apis + extension: .ts + backend: false + index: + templatePath: frontend/index.ftl + packageName: src/views + extension: .vue + backend: false + AddModal: + templatePath: frontend/AddModal.ftl + packageName: src/views + extension: .vue + backend: false + DetailDrawer: + templatePath: frontend/DetailDrawer.ftl + packageName: src/views + extension: .vue + backend: false + Menu: + template-path: backend/Menu.ftl + packageName: sql + extension: .sql + backend: true \ No newline at end of file diff --git a/mes-webapi/src/main/resources/config/application-prod.yml b/mes-webapi/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..0006678 --- /dev/null +++ b/mes-webapi/src/main/resources/config/application-prod.yml @@ -0,0 +1,318 @@ +--- ### 项目配置 +project: + # URL(跨域配置默认放行此 URL,第三方登录回调默认使用此 URL 为前缀,请注意更改为你实际的前端 URL) + url: https://admin.wms.top + # 是否为生产环境 + production: true + +--- ### 服务器配置 +server: + # HTTP 端口(默认 8080) + port: 18000 + +--- ### 数据源配置 +spring.datasource: + type: com.zaxxer.hikari.HikariDataSource + # 请务必提前创建好名为 wms_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置 + url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:wms_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + username: ${DB_USER:root} + password: ${DB_PWD:123456} + driver-class-name: com.mysql.cj.jdbc.Driver +# # PostgreSQL 配置 +# url: jdbc:postgresql://${DB_HOST:127.0.0.1}:${DB_PORT:5432}/${DB_NAME:wms_admin}?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&stringtype=unspecified +# username: ${DB_USER:root} +# password: ${DB_PWD:123456} +# driver-class-name: org.postgresql.Driver + # Hikari 连接池配置 + hikari: + # 最大连接数量(默认 10,根据实际环境调整) + # 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒 + maximum-pool-size: 20 + # 获取连接超时时间(默认 30000 毫秒,30 秒) + connection-timeout: 30000 + # 空闲连接最大存活时间(默认 600000 毫秒,10 分钟) + idle-timeout: 600000 + # 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime(默认 0,禁用) + keepaliveTime: 30000 + # 连接最大生存时间(默认 1800000 毫秒,30 分钟) + max-lifetime: 1800000 +## Liquibase 配置 +spring.liquibase: + # 是否启用 + enabled: true + # 配置文件路径 + change-log: classpath:/db/changelog/db.changelog-master.yaml + +--- ### 缓存配置 +spring.data: + ## Redis 配置(单机模式) + redis: + # 地址 + host: ${REDIS_HOST:127.0.0.1} + # 端口(默认 6379) + port: ${REDIS_PORT:6379} + # 密码(未设置密码时请注释掉) + password: ${REDIS_PWD:123456} + # 数据库索引 + database: ${REDIS_DB:0} + # 连接超时时间 + timeout: 10s + # 是否开启 SSL + ssl: + enabled: false + ## Redisson 配置 + redisson: + enabled: true + mode: SINGLE +## JetCache 配置 +jetcache: + # 统计间隔(默认 0,表示不统计) + statIntervalMinutes: 0 + ## 本地/进程级/一级缓存配置 + local: + default: + # 缓存类型 + type: caffeine + # key 转换器的全局配置 + keyConvertor: jackson + # 以毫秒为单位指定超时时间的全局配置 + expireAfterWriteInMillis: 7200000 + # 每个缓存实例的最大元素的全局配置,仅 local 类型的缓存需要指定 + limit: 1000 + ## 远程/分布式/二级缓存配置 + remote: + default: + # 缓存类型 + type: redisson + # key 转换器的全局配置(用于将复杂的 KEY 类型转换为缓存实现可以接受的类型) + keyConvertor: jackson + # 以毫秒为单位指定超时时间的全局配置 + expireAfterWriteInMillis: 7200000 + # 2.7+ 支持两级缓存更新以后失效其他 JVM 中的 local cache,但多个服务共用 Redis 同一个 channel 可能会造成广播风暴,需要在这里指定channel。 + # 你可以决定多个不同的服务是否共用同一个 channel,如果没有指定则不开启。 + broadcastChannel: ${spring.application.name} + # 序列化器的全局配置,仅 remote 类型的缓存需要指定 + valueEncoder: java + valueDecoder: java + +--- ### 验证码配置 +continew-starter.captcha: + ## 行为验证码 + behavior: + enabled: true + cache-type: REDIS + water-mark: + # 一分钟内接口请求次数限制开关(默认:0,关闭,开启后下方失败锁定配置才会生效) + req-frequency-limit-enable: 0 + # 一分钟内验证码最多失败次数限制(默认:5次) + req-get-lock-limit: 5 + # 一分钟内验证码最多失败次数限制达标后锁定时间(默认:300秒) + req-get-lock-seconds: 300 + ## 图形验证码 + graphic: + # 类型 + type: SPEC + # 内容长度 + length: 4 + # 过期时间 + expirationInMinutes: 2 +## 其他验证码配置 +captcha: + ## 邮箱验证码配置 + mail: + # 内容长度 + length: 6 + # 过期时间 + expirationInMinutes: 5 + # 模板路径 + templatePath: mail/captcha.ftl + ## 短信验证码配置 + sms: + # 内容长度 + length: 4 + # 过期时间 + expirationInMinutes: 5 + # 模板 ID + templateId: 1 + +--- ### 日志配置 +continew-starter.log: + # 是否打印日志,开启后可打印访问日志(类似于 Nginx access log) + is-print: false +## 项目日志配置(配置重叠部分,优先级高于 logback-spring.xml 中的配置) +logging: + level: + top.wms.admin: INFO + top.wms.starter: INFO + file: + path: ../logs + +--- ### 跨域配置 +continew-starter.web.cors: + enabled: true + # 配置允许跨域的域名 + allowed-origins: + - ${project.url} + # 配置允许跨域的请求方式 + allowed-methods: '*' + # 配置允许跨域的请求头 + allowed-headers: '*' + # 配置允许跨域的响应头 + exposed-headers: '*' + +--- ### 接口文档配置 +## 接口文档增强配置 +knife4j: + # 开启生产环境屏蔽 + production: ${project.production} + +--- ### WebSocket 配置 +continew-starter.messaging.websocket: + enabled: true + path: /websocket + # 配置允许跨域的域名 + allowed-origins: + - ${project.url} + +--- ### 短信配置 +sms: + # 从 YAML 读取配置 + config-type: YAML + is-print: false + blends: + cloopen: + # 短信厂商 + supplier: cloopen + base-url: https://app.cloopen.com:8883/2013-12-26 + access-key-id: 你的Access Key + access-key-secret: 你的Access Key Secret + sdk-app-id: 你的应用ID + +--- ### 邮件配置 +spring.mail: + # 根据需要更换 + host: smtp.126.com + port: 465 + username: 你的邮箱 + password: 你的邮箱授权码 + properties: + mail: + smtp: + auth: true + socketFactory: + class: javax.net.ssl.SSLSocketFactory + port: 465 + +--- ### Just Auth 配置 +justauth: + enabled: true + type: + GITEE: + client-id: 5d271b7f638941812aaf8bfc2e2f08f06d6235ef934e0e39537e2364eb8452c4 + client-secret: 1f7d08**********5b7**********29e + redirect-uri: ${project.url}/social/callback?source=gitee + GITHUB: + client-id: 38080dad08cfbdfacca9 + client-secret: 1f7d08**********5b7**********29e + redirect-uri: ${project.url}/social/callback?source=github + cache: + type: REDIS + +--- ### Sa-Token 扩展配置 +sa-token.extension: + # 安全配置:排除(放行)路径配置 + security.excludes: + - /error + # 静态资源 + - /*.html + - /*/*.html + - /*/*.css + - /*/*.js + - /websocket/** + # 本地存储资源 + - /file/** + +--- ### 安全配置 +continew-starter.security: + ## 字段加/解密配置 + crypto: + enabled: true + # 对称加密算法密钥 + password: abcdefghijklmnop + # 非对称加密算法密钥(在线生成 RSA 密钥对:http://web.chacuo.net/netrsakeypair) + public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9uaUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ== + private-key: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV + ## 密码编码器配置 + password: + enabled: true + # BCryptPasswordEncoder + encoding-id: bcrypt + ## 限流器配置 + limiter: + enabled: true + key-prefix: RateLimiter + +--- ### 文件上传配置 +spring.servlet: + multipart: + enabled: true + # 单文件上传大小限制 + max-file-size: 10MB + # 单次总上传文件大小限制 + max-request-size: 20MB +## 头像配置 +avatar: + # 存储路径 + path: user/avatar/ + # 支持的后缀 + support-suffix: jpg,jpeg,png,gif + +--- ### Snail Job 配置 +snail-job: + # 客户端地址(默认自动获取本机 IP) + #host: 127.0.0.1 + # 客户端端口(默认:1789) + port: 1789 + # 命名空间 ID + namespace: ${SCHEDULE_NAMESPACE:764d604ec6fc45f68cd92514c40e9e1a} + # 分组名 + group: ${SCHEDULE_GROUP:wms-admin} + # 令牌 + token: ${SCHEDULE_TOKEN:SJ_Wyz3dmsdbDOkDujOTSSoBjGQP1BMsVnj} + ## 服务端配置(任务调度中心) + server: + # 服务端地址,若服务端集群部署则此处配置域名 + host: ${SCHEDULE_HOST:127.0.0.1} + # Netty 端口号 + port: ${SCHEDULE_PORT:1788} + # API 配置 + api: + # URL + url: http://127.0.0.1:8001/snail-job + # 用户名 + username: ${SCHEDULE_USERNAME:admin} + # 密码 + password: ${SCHEDULE_PASSWORD:admin} + ## 重试数据批量上报滑动窗口配置 + retry: + reportSlidingWindow: + # 窗口期单位 + chrono-unit: SECONDS + # 窗口期时间长度 + duration: 10 + # 总量窗口期阈值 + total-threshold: 50 + # 窗口数量预警 + window-total-threshold: 150 + ## 调度线程池配置 + dispatcherThreadPool: + # 核心线程数 + corePoolSize: 16 + # 最大线程数 + maximumPoolSize: 16 + # 线程存活时间 + keepAliveTime: 1 + # 时间单位 + timeUnit: SECONDS + # 队列容量 + queueCapacity: 10000 \ No newline at end of file diff --git a/mes-webapi/src/main/resources/config/application.yml b/mes-webapi/src/main/resources/config/application.yml new file mode 100644 index 0000000..ca2a859 --- /dev/null +++ b/mes-webapi/src/main/resources/config/application.yml @@ -0,0 +1,291 @@ +--- ### 项目配置 +project: + # 名称 + name: Wms Admin + # 应用名称 + app-name: wms-admin + # 版本 + version: 3.6.0-SNAPSHOT + # 描述 + description: 持续迭代优化的前后端分离中后台管理系统框架,开箱即用,持续提供舒适的开发体验。 + # 基本包 + base-package: top.wms.admin + ## 作者信息配置 + contact: + name: Charles7c + email: charles7c@126.com + url: https://blog.charles7c.top/about/me + ## 许可协议信息配置 + license: + name: Apache-2.0 + url: https://github.com/wms-org/wms-admin/blob/dev/LICENSE + +--- ### 日志配置 +continew-starter.log: + # 包含信息 + includes: + - DESCRIPTION + - MODULE + - REQUEST_HEADERS + - REQUEST_BODY + - IP_ADDRESS + - BROWSER + - OS + - RESPONSE_HEADERS + - RESPONSE_BODY +## 项目日志配置 +logging: + config: classpath:logback-spring.xml + +--- ### 链路跟踪配置 +continew-starter.web: + trace: + enabled: false + trace-id-name: traceId + +--- ### 全局响应配置 +continew-starter.web: + response: + # 是否开启国际化(默认:false) + i18n: false + # 自定义失败 HTTP 状态码(默认:200,建议业务和通信状态码区分) + default-http-status-code-on-error: 200 + # 自定义成功响应码(默认:0) + default-success-code: 0 + # 自定义成功提示(默认:ok) + default-success-msg: ok + # 自定义失败响应码(默认:1) + default-error-code: 1 + # 自定义失败提示(默认:error) + default-error-msg: error + # 是否将原生异常错误信息填充到状态信息中 + origin-exception-using-detail-message: false + # 响应类全名(配置后 response-style 将不再生效) + response-class-full-name: top.continew.starter.web.model.R + +--- ### 全局树结构配置(简单树,对应前端 UI) +continew-starter.crud: + tree: + id-key: key + name-key: title + weight-key: sort + +--- ### 接口文档配置 +springdoc: + # 设置对象型参数的展示形式(设为 true 表示将对象型参数平展开,即对象内的属性直接作为参数展示而不是嵌套在对象内,默认 false) + # 如果不添加该全局配置,可以在需要如此处理的对象参数类上使用 @ParameterObject + default-flat-param-object: true + # 分组配置 + group-configs: + - group: all + paths-to-match: /** + paths-to-exclude: + - /error + - group: auth + display-name: 系统认证 + packages-to-scan: ${project.base-package}.controller.auth + - group: common + display-name: 通用接口 + packages-to-scan: ${project.base-package}.controller.common + - group: system + display-name: 系统管理 + packages-to-scan: ${project.base-package}.controller.system + - group: monitor + display-name: 系统监控 + packages-to-scan: ${project.base-package}.controller.monitor + - group: schedule + display-name: 任务调度 + packages-to-scan: ${project.base-package}.controller.schedule + - group: open + display-name: 能力开放 + packages-to-scan: ${project.base-package}.controller.open + - group: code + display-name: 代码生成 + packages-to-scan: ${project.base-package}.controller.code + ## 组件配置 + components: + # 鉴权配置 + security-schemes: + Authorization: + type: HTTP + in: HEADER + name: ${sa-token.token-name} + scheme: ${sa-token.token-prefix} +## 接口文档增强配置 +knife4j: + enable: true + setting: + # 是否显示默认的 footer(默认 true,显示) + enable-footer: false + # 是否自定义 footer(默认 false,非自定义) + enable-footer-custom: true + # 自定义 footer 内容,支持 Markdown 语法 + footer-custom-content: 'Copyright © 2022-present [${project.contact.name}](${project.contact.url}) ⋅ [${project.name}](${project.url}) v${project.version}' + +--- ### Sa-Token 配置 +sa-token: + # Token 名称(同时也是 cookie 名称) + token-name: Authorization + # Token 有效期(单位:秒,默认 30 天,-1 代表永不过期) + timeout: 86400 + # Token 最低活跃频率(单位:秒,默认 -1,代表不限制,永不冻结。如果 token 超过此时间没有访问系统就会被冻结) + active-timeout: 1800 + # 是否打开自动续签(如果此值为 true,框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作) + auto-renew: true + # 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 Token(为 true 时所有登录共用一个 Token,为 false 时每次登录新建一个 Token) + is-share: false + # 是否输出操作日志 + is-log: false + # 是否启用动态 activeTimeout 功能 + dynamic-active-timeout: true + # JWT 秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk + ## 扩展配置 + extension: + enabled: true + enableJwt: true + # 持久层配置 + dao.type: REDIS + +--- ### MyBatis Plus 配置 +mybatis-plus: + # Mapper XML 文件目录配置 + mapper-locations: classpath*:/mapper/**/*Mapper.xml + # 类型别名扫描包配置 + type-aliases-package: ${project.base-package}.**.model + ## MyBatis 配置 + configuration: + # MyBatis 自动映射策略 + # NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射 + auto-mapping-behavior: PARTIAL + ## 全局配置 + global-config: + banner: true + db-config: + # 主键类型(默认 assign_id,表示自行赋值) + # auto 代表使用数据库自增策略(需要在表中设置好自增约束) +# id-type: ASSIGN_ID + id-type: AUTO + # 逻辑删除字段 + logic-delete-field: isDeleted + # 逻辑删除全局值(默认 1,表示已删除) + logic-delete-value: 1 + # 逻辑未删除全局值(默认 0,表示未删除) + logic-not-delete-value: 0 + ## 扩展配置 + extension: + enabled: true + # Mapper 接口扫描包配置 + mapper-package: ${project.base-package}.**.mapper + # ID 生成器配置 + id-generator: + type: COSID + # 分页插件配置 + pagination: + enabled: true + db-type: MYSQL + +--- ### CosId 配置 +cosid: + namespace: ${spring.application.name} + machine: + enabled: true + # 机器号分配器 + distributor: + type: REDIS + guarder: + # 开启机器号守护 + enabled: true + snowflake: + enabled: true + zone-id: Asia/Shanghai + epoch: 1577203200000 + share: + # 开启时钟回拨同步 + clock-sync: true + friendly: true + provider: + safe-js: + machine-bit: 7 + sequence-bit: 9 + +--- ### 认证配置 +auth: + ## 密码配置 + password: + excludes: + - /auth/user/route + - /auth/user/info + - /auth/logout + - /system/user/password + - /ws/scale + +--- ### 服务器配置 +server: + servlet: + # 应用访问路径 + context-path: / + ## Undertow 服务器配置 + undertow: + # HTTP POST 请求内容的大小上限(默认 -1,不限制) + max-http-post-size: -1 + # 以下的配置会影响 buffer,这些 buffer 会用于服务器连接的 IO 操作,有点类似 Netty 的池化内存管理 + # 每块 buffer的空间大小(越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可) + buffer-size: 512 + # 是否分配的直接内存(NIO 直接分配的堆外内存) + direct-buffers: true + threads: + # 设置 IO 线程数,它主要执行非阻塞的任务,它们会负责多个连接(默认每个 CPU 核心一个线程) + io: 8 + # 阻塞任务线程池,当执行类似 Servlet 请求阻塞操作,Undertow 会从这个线程池中取得线程(它的值设置取决于系统的负载) + worker: 256 + +--- ### Spring 配置 +spring: + application: + name: ${project.app-name} + main: + # 允许定义重名的 bean 对象覆盖原有的 bean + allow-bean-definition-overriding: true + # 允许循环依赖 + allow-circular-references: true + ## 环境配置 + profiles: + # 启用的环境 + active: dev + include: + - generator + ## 线程池配置(默认启用扩展配置,如未指定 corePoolSize、maxPoolSize 则根据机器配置自动设置) + task: + # 异步任务 + execution: + thread-name-prefix: task-pool + # 任务拒绝策略(默认 ABORT,不执行新任务,直接抛出 RejectedExecutionException 异常) + # CALLER_RUNS:提交的任务在执行被拒绝时,会由提交任务的线程去执行 + rejected-policy: CALLER_RUNS + pool: + keep-alive: 300s + shutdown: + # 是否等待任务执行完成再关闭线程池(默认 false) + await-termination: true + # 等待时间 + await-termination-period: 30s + # 定时任务 + scheduling: + thread-name-prefix: schedule-pool + # 任务拒绝策略(默认 ABORT,不执行新任务,直接抛出 RejectedExecutionException 异常) + # CALLER_RUNS:提交的任务在执行被拒绝时,会由提交任务的线程去执行 + rejected-policy: CALLER_RUNS + shutdown: + # 是否等待任务执行完成再关闭线程池(默认 false) + await-termination: true + # 等待时间 + await-termination-period: 30s + +--- ### 健康检查配置 +management.health: + mail: + # 关闭邮箱健康检查(邮箱配置错误或邮箱服务器不可用时,健康检查会报错) + enabled: false \ No newline at end of file diff --git a/mes-webapi/src/main/resources/db/changelog/db.changelog-master.yaml b/mes-webapi/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..e800a80 --- /dev/null +++ b/mes-webapi/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,26 @@ +databaseChangeLog: +# - include: +# file: db/changelog/mysql/main_table.sql +# - include: +# file: db/changelog/mysql/main_column.sql +# - include: +# file: db/changelog/mysql/main_data.sql +# - include: +# file: db/changelog/mysql/plugin/plugin_schedule.sql +# - include: +# file: db/changelog/mysql/plugin/plugin_open.sql +# - include: +# file: db/changelog/mysql/plugin/plugin_generator.sql +# PostgreSQL +# - include: +# file: db/changelog/postgresql/main_table.sql +# - include: +# file: db/changelog/postgresql/main_column.sql +# - include: +# file: db/changelog/postgresql/main_data.sql +# - include: +# file: db/changelog/postgresql/plugin/plugin_schedule.sql +# - include: +# file: db/changelog/postgresql/plugin/plugin_open.sql +# - include: +# file: db/changelog/postgresql/plugin/plugin_generator.sql \ No newline at end of file diff --git a/mes-webapi/src/main/resources/favicon.ico b/mes-webapi/src/main/resources/favicon.ico new file mode 100644 index 0000000..f3c48de Binary files /dev/null and b/mes-webapi/src/main/resources/favicon.ico differ diff --git a/mes-webapi/src/main/resources/logback-spring.xml b/mes-webapi/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..cbb6a0a --- /dev/null +++ b/mes-webapi/src/main/resources/logback-spring.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + ${LOG_CHARSET} + + + + + + + ${FILE_LOG_PATTERN} + ${LOG_CHARSET} + + + + + + + ${LOG_PATH}/${APP_NAME}.log + + + + ${LOG_PATH}/%d{yyyy-MM-dd}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz + + ${FILE_MAX_SIZE} + + ${FILE_MAX_HISTORY} + + + ${FILE_LOG_PATTERN} + ${LOG_CHARSET} + + + + + + + 0 + + 512 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mes-webapi/src/main/resources/templates/import/batch.xlsx b/mes-webapi/src/main/resources/templates/import/batch.xlsx new file mode 100644 index 0000000..9fac4ed Binary files /dev/null and b/mes-webapi/src/main/resources/templates/import/batch.xlsx differ diff --git a/mes-webapi/src/main/resources/templates/import/materialInfo.xlsx b/mes-webapi/src/main/resources/templates/import/materialInfo.xlsx new file mode 100644 index 0000000..d08b436 Binary files /dev/null and b/mes-webapi/src/main/resources/templates/import/materialInfo.xlsx differ diff --git a/mes-webapi/src/main/resources/templates/import/materialProcess.xlsx b/mes-webapi/src/main/resources/templates/import/materialProcess.xlsx new file mode 100644 index 0000000..2e88fd8 Binary files /dev/null and b/mes-webapi/src/main/resources/templates/import/materialProcess.xlsx differ diff --git a/mes-webapi/src/main/resources/templates/import/materialType.xlsx b/mes-webapi/src/main/resources/templates/import/materialType.xlsx new file mode 100644 index 0000000..072a995 Binary files /dev/null and b/mes-webapi/src/main/resources/templates/import/materialType.xlsx differ diff --git a/mes-webapi/src/main/resources/templates/import/user.xlsx b/mes-webapi/src/main/resources/templates/import/user.xlsx new file mode 100644 index 0000000..03a9360 Binary files /dev/null and b/mes-webapi/src/main/resources/templates/import/user.xlsx differ diff --git a/mes-webapi/src/main/resources/templates/mail/captcha.ftl b/mes-webapi/src/main/resources/templates/mail/captcha.ftl new file mode 100644 index 0000000..4d4636c --- /dev/null +++ b/mes-webapi/src/main/resources/templates/mail/captcha.ftl @@ -0,0 +1,47 @@ + + + + + + + + + + + +
+
+ +
+
+

亲爱的用户:

+

+ 您好!感谢您使用 ${siteTitle},本次请求的验证码为:${captcha},请在 ${expiration} 分钟内使用此验证码完成验证。 +

+
+

Dear user:

+

+ Hello! Thanks for using ${siteTitle}, The verification code for this request is: ${captcha}, please use this verification code to complete the verification within ${expiration} minutes. +

+
+
+

+ 若非本人操作,请忽略此邮件。此邮件由系统自动发送,请勿直接回复该邮件。
+ Please ignore this email if not by yourself. This email is sent automatically by the system, please do not reply to this email directly. +

+

${siteCopyright}

+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/mes-webapi/src/test/java/top/mes/admin/WmsAdminApplicationTests.java b/mes-webapi/src/test/java/top/mes/admin/WmsAdminApplicationTests.java new file mode 100644 index 0000000..966e682 --- /dev/null +++ b/mes-webapi/src/test/java/top/mes/admin/WmsAdminApplicationTests.java @@ -0,0 +1,12 @@ +package top.wms.admin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WmsAdminApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..84151b5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,257 @@ + + + 4.0.0 + + + top.continew + continew-starter + 2.9.0 + + + top.wms + wms-admin + ${revision} + pom + 园区管理系统 + https://github.com/wms-org/wms-admin + + + wms-webapi + wms-module-system + wms-plugin + wms-common + wms-extension + + + + + 3.6.0-SNAPSHOT + + + + + + + + top.wms + wms-webapi + ${revision} + + + + + top.wms + wms-module-system + ${revision} + + + + + top.wms + wms-common + ${revision} + + + + + top.wms + wms-plugin-schedule + ${revision} + + + + + top.wms + wms-plugin-generator + ${revision} + + + + + + + + cn.hutool + hutool-all + + + + + org.projectlombok + lombok + true + + + + + org.mapstruct + mapstruct + 1.5.5.Final + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + + + + commons-fileupload + commons-fileupload + 1.4 + + + + + commons-io + commons-io + 2.8.0 + + + + com.google.guava + guava + 29.0-jre + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + -parameters + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + + com.diffplug.spotless + spotless-maven-plugin + + + compile + + apply + + + + + + + + .style/p3c-codestyle.xml + + + .style/license-header + + + + + + + org.codehaus.mojo + flatten-maven-plugin + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten-clean + clean + + clean + + + + + + + + + + + sonar + + https://sonarcloud.io + charles7c + Charles7c_wms-admin + ${project.groupId}:${project.artifactId} + + + false + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + + + verify + + sonar + + + + + + + + + + + + + huawei-mirror + HuaweiCloud Mirror + https://mirrors.huaweicloud.com/repository/maven/ + + + ali-mirror + AliYun Mirror + https://maven.aliyun.com/repository/public/ + + + + + + + huawei-mirror + HuaweiCloud Mirror + https://mirrors.huaweicloud.com/repository/maven/ + + + ali-mirror + AliYun Mirror + https://maven.aliyun.com/repository/public/ + + +